Compare commits
7 Commits
main
...
feat/merca
Author | SHA1 | Date | |
---|---|---|---|
|
8860fa0cae | ||
|
9ebc65e8c4 | ||
|
8580c29a92 | ||
|
94498ea7af | ||
|
d331d8c5e4 | ||
|
f26368f405 | ||
|
7be836d834 |
|
@ -122,3 +122,9 @@ ZOHOCRM_CLIENT_ID=""
|
||||||
ZOHOCRM_CLIENT_SECRET=""
|
ZOHOCRM_CLIENT_SECRET=""
|
||||||
|
|
||||||
# *********************************************************************************************************
|
# *********************************************************************************************************
|
||||||
|
|
||||||
|
|
||||||
|
# - MERCADOPAGO PAYMENT
|
||||||
|
# Used for the MercadoPago payment
|
||||||
|
NEXT_PUBLIC_MERCADO_PAGO_PUBLIC_KEY=""
|
||||||
|
MERCADO_PAGO_ACCESS_TOKEN=""
|
|
@ -0,0 +1 @@
|
||||||
|
Subproject commit 604d937661ed8f8fd50cc645bf7d129b635333e8
|
|
@ -0,0 +1 @@
|
||||||
|
Subproject commit 8e116959652a773fa7cde5e9f8bf502c054b151b
|
|
@ -85,6 +85,7 @@
|
||||||
"markdown-it": "^13.0.1",
|
"markdown-it": "^13.0.1",
|
||||||
"md5": "^2.3.0",
|
"md5": "^2.3.0",
|
||||||
"memory-cache": "^0.2.0",
|
"memory-cache": "^0.2.0",
|
||||||
|
"mercadopago": "^1.5.14",
|
||||||
"micro": "^10.0.1",
|
"micro": "^10.0.1",
|
||||||
"mime-types": "^2.1.35",
|
"mime-types": "^2.1.35",
|
||||||
"next": "^13.2.1",
|
"next": "^13.2.1",
|
||||||
|
|
|
@ -1799,6 +1799,7 @@
|
||||||
"charge_attendee": "Charge attendee {{amount, currency}}",
|
"charge_attendee": "Charge attendee {{amount, currency}}",
|
||||||
"payment_app_commission": "Require payment ({{paymentFeePercentage}}% + {{fee, currency}} commission per transaction)",
|
"payment_app_commission": "Require payment ({{paymentFeePercentage}}% + {{fee, currency}} commission per transaction)",
|
||||||
"email_invite_team": "{{email}} has been invited",
|
"email_invite_team": "{{email}} has been invited",
|
||||||
"error_collecting_card": "Error collecting card",
|
"image_size_limit_exceed": "Uploaded image shouldn't exceed 5mb size limit",
|
||||||
"image_size_limit_exceed": "Uploaded image shouldn't exceed 5mb size limit"
|
"currency": "Currency",
|
||||||
|
"error_collecting_card": "Error collecting card"
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
Subproject commit a4b58535c2716815b18cd88e7d478570012b739d
|
|
@ -11,6 +11,7 @@ export const AppSetupMap = {
|
||||||
zapier: dynamic(() => import("../../zapier/pages/setup")),
|
zapier: dynamic(() => import("../../zapier/pages/setup")),
|
||||||
closecom: dynamic(() => import("../../closecom/pages/setup")),
|
closecom: dynamic(() => import("../../closecom/pages/setup")),
|
||||||
sendgrid: dynamic(() => import("../../sendgrid/pages/setup")),
|
sendgrid: dynamic(() => import("../../sendgrid/pages/setup")),
|
||||||
|
mercado_pago: dynamic(() => import("../../mercado_pago/pages/setup")),
|
||||||
};
|
};
|
||||||
|
|
||||||
export const AppSetupPage = (props: { slug: string }) => {
|
export const AppSetupPage = (props: { slug: string }) => {
|
||||||
|
|
|
@ -24,6 +24,7 @@ export const EventTypeAddonMap = {
|
||||||
ga4: dynamic(() => import("./ga4/components/EventTypeAppCardInterface")),
|
ga4: dynamic(() => import("./ga4/components/EventTypeAppCardInterface")),
|
||||||
giphy: dynamic(() => import("./giphy/components/EventTypeAppCardInterface")),
|
giphy: dynamic(() => import("./giphy/components/EventTypeAppCardInterface")),
|
||||||
gtm: dynamic(() => import("./gtm/components/EventTypeAppCardInterface")),
|
gtm: dynamic(() => import("./gtm/components/EventTypeAppCardInterface")),
|
||||||
|
mercado_pago: dynamic(() => import("./mercado_pago/components/EventTypeAppCardInterface")),
|
||||||
metapixel: dynamic(() => import("./metapixel/components/EventTypeAppCardInterface")),
|
metapixel: dynamic(() => import("./metapixel/components/EventTypeAppCardInterface")),
|
||||||
plausible: dynamic(() => import("./plausible/components/EventTypeAppCardInterface")),
|
plausible: dynamic(() => import("./plausible/components/EventTypeAppCardInterface")),
|
||||||
qr_code: dynamic(() => import("./qr_code/components/EventTypeAppCardInterface")),
|
qr_code: dynamic(() => import("./qr_code/components/EventTypeAppCardInterface")),
|
||||||
|
|
|
@ -10,6 +10,7 @@ import { appKeysSchema as googlecalendar_zod_ts } from "./googlecalendar/zod";
|
||||||
import { appKeysSchema as gtm_zod_ts } from "./gtm/zod";
|
import { appKeysSchema as gtm_zod_ts } from "./gtm/zod";
|
||||||
import { appKeysSchema as hubspot_zod_ts } from "./hubspot/zod";
|
import { appKeysSchema as hubspot_zod_ts } from "./hubspot/zod";
|
||||||
import { appKeysSchema as larkcalendar_zod_ts } from "./larkcalendar/zod";
|
import { appKeysSchema as larkcalendar_zod_ts } from "./larkcalendar/zod";
|
||||||
|
import { appKeysSchema as mercado_pago_zod_ts } from "./mercado_pago/zod";
|
||||||
import { appKeysSchema as metapixel_zod_ts } from "./metapixel/zod";
|
import { appKeysSchema as metapixel_zod_ts } from "./metapixel/zod";
|
||||||
import { appKeysSchema as office365calendar_zod_ts } from "./office365calendar/zod";
|
import { appKeysSchema as office365calendar_zod_ts } from "./office365calendar/zod";
|
||||||
import { appKeysSchema as office365video_zod_ts } from "./office365video/zod";
|
import { appKeysSchema as office365video_zod_ts } from "./office365video/zod";
|
||||||
|
@ -37,6 +38,7 @@ export const appKeysSchemas = {
|
||||||
gtm: gtm_zod_ts,
|
gtm: gtm_zod_ts,
|
||||||
hubspot: hubspot_zod_ts,
|
hubspot: hubspot_zod_ts,
|
||||||
larkcalendar: larkcalendar_zod_ts,
|
larkcalendar: larkcalendar_zod_ts,
|
||||||
|
mercado_pago: mercado_pago_zod_ts,
|
||||||
metapixel: metapixel_zod_ts,
|
metapixel: metapixel_zod_ts,
|
||||||
office365calendar: office365calendar_zod_ts,
|
office365calendar: office365calendar_zod_ts,
|
||||||
office365video: office365video_zod_ts,
|
office365video: office365video_zod_ts,
|
||||||
|
|
|
@ -25,6 +25,7 @@ import { metadata as hubspot__metadata_ts } from "./hubspot/_metadata";
|
||||||
import { metadata as huddle01video__metadata_ts } from "./huddle01video/_metadata";
|
import { metadata as huddle01video__metadata_ts } from "./huddle01video/_metadata";
|
||||||
import { metadata as jitsivideo__metadata_ts } from "./jitsivideo/_metadata";
|
import { metadata as jitsivideo__metadata_ts } from "./jitsivideo/_metadata";
|
||||||
import { metadata as larkcalendar__metadata_ts } from "./larkcalendar/_metadata";
|
import { metadata as larkcalendar__metadata_ts } from "./larkcalendar/_metadata";
|
||||||
|
import mercado_pago_config_json from "./mercado_pago/config.json";
|
||||||
import metapixel_config_json from "./metapixel/config.json";
|
import metapixel_config_json from "./metapixel/config.json";
|
||||||
import n8n_config_json from "./n8n/config.json";
|
import n8n_config_json from "./n8n/config.json";
|
||||||
import { metadata as office365calendar__metadata_ts } from "./office365calendar/_metadata";
|
import { metadata as office365calendar__metadata_ts } from "./office365calendar/_metadata";
|
||||||
|
@ -87,6 +88,7 @@ export const appStoreMetadata = {
|
||||||
huddle01video: huddle01video__metadata_ts,
|
huddle01video: huddle01video__metadata_ts,
|
||||||
jitsivideo: jitsivideo__metadata_ts,
|
jitsivideo: jitsivideo__metadata_ts,
|
||||||
larkcalendar: larkcalendar__metadata_ts,
|
larkcalendar: larkcalendar__metadata_ts,
|
||||||
|
mercado_pago: mercado_pago_config_json,
|
||||||
metapixel: metapixel_config_json,
|
metapixel: metapixel_config_json,
|
||||||
n8n: n8n_config_json,
|
n8n: n8n_config_json,
|
||||||
office365calendar: office365calendar__metadata_ts,
|
office365calendar: office365calendar__metadata_ts,
|
||||||
|
|
|
@ -10,6 +10,7 @@ import { appDataSchema as googlecalendar_zod_ts } from "./googlecalendar/zod";
|
||||||
import { appDataSchema as gtm_zod_ts } from "./gtm/zod";
|
import { appDataSchema as gtm_zod_ts } from "./gtm/zod";
|
||||||
import { appDataSchema as hubspot_zod_ts } from "./hubspot/zod";
|
import { appDataSchema as hubspot_zod_ts } from "./hubspot/zod";
|
||||||
import { appDataSchema as larkcalendar_zod_ts } from "./larkcalendar/zod";
|
import { appDataSchema as larkcalendar_zod_ts } from "./larkcalendar/zod";
|
||||||
|
import { appDataSchema as mercado_pago_zod_ts } from "./mercado_pago/zod";
|
||||||
import { appDataSchema as metapixel_zod_ts } from "./metapixel/zod";
|
import { appDataSchema as metapixel_zod_ts } from "./metapixel/zod";
|
||||||
import { appDataSchema as office365calendar_zod_ts } from "./office365calendar/zod";
|
import { appDataSchema as office365calendar_zod_ts } from "./office365calendar/zod";
|
||||||
import { appDataSchema as office365video_zod_ts } from "./office365video/zod";
|
import { appDataSchema as office365video_zod_ts } from "./office365video/zod";
|
||||||
|
@ -37,6 +38,7 @@ export const appDataSchemas = {
|
||||||
gtm: gtm_zod_ts,
|
gtm: gtm_zod_ts,
|
||||||
hubspot: hubspot_zod_ts,
|
hubspot: hubspot_zod_ts,
|
||||||
larkcalendar: larkcalendar_zod_ts,
|
larkcalendar: larkcalendar_zod_ts,
|
||||||
|
mercado_pago: mercado_pago_zod_ts,
|
||||||
metapixel: metapixel_zod_ts,
|
metapixel: metapixel_zod_ts,
|
||||||
office365calendar: office365calendar_zod_ts,
|
office365calendar: office365calendar_zod_ts,
|
||||||
office365video: office365video_zod_ts,
|
office365video: office365video_zod_ts,
|
||||||
|
|
|
@ -25,6 +25,7 @@ export const apiHandlers = {
|
||||||
huddle01video: import("./huddle01video/api"),
|
huddle01video: import("./huddle01video/api"),
|
||||||
jitsivideo: import("./jitsivideo/api"),
|
jitsivideo: import("./jitsivideo/api"),
|
||||||
larkcalendar: import("./larkcalendar/api"),
|
larkcalendar: import("./larkcalendar/api"),
|
||||||
|
mercado_pago: import("./mercado_pago/api"),
|
||||||
metapixel: import("./metapixel/api"),
|
metapixel: import("./metapixel/api"),
|
||||||
n8n: import("./n8n/api"),
|
n8n: import("./n8n/api"),
|
||||||
office365calendar: import("./office365calendar/api"),
|
office365calendar: import("./office365calendar/api"),
|
||||||
|
|
|
@ -10,6 +10,7 @@ const appStore = {
|
||||||
huddle01video: import("./huddle01video"),
|
huddle01video: import("./huddle01video"),
|
||||||
jitsivideo: import("./jitsivideo"),
|
jitsivideo: import("./jitsivideo"),
|
||||||
larkcalendar: import("./larkcalendar"),
|
larkcalendar: import("./larkcalendar"),
|
||||||
|
mercado_pago: import("./mercado_pago"),
|
||||||
office365calendar: import("./office365calendar"),
|
office365calendar: import("./office365calendar"),
|
||||||
office365video: import("./office365video"),
|
office365video: import("./office365video"),
|
||||||
plausible: import("./plausible"),
|
plausible: import("./plausible"),
|
||||||
|
|
|
@ -0,0 +1,9 @@
|
||||||
|
---
|
||||||
|
description: .
|
||||||
|
items:
|
||||||
|
- /api/app-store/mercado_pago/1.jpg
|
||||||
|
- /api/app-store/mercado_pago/2.jpg
|
||||||
|
- /api/app-store/mercado_pago/3.jpg
|
||||||
|
---
|
||||||
|
|
||||||
|
.
|
|
@ -0,0 +1,10 @@
|
||||||
|
import type { AppMeta } from "@calcom/types/App";
|
||||||
|
|
||||||
|
import config from "./config.json";
|
||||||
|
|
||||||
|
export const metadata = {
|
||||||
|
category: "other",
|
||||||
|
...config,
|
||||||
|
} as AppMeta;
|
||||||
|
|
||||||
|
export default metadata;
|
|
@ -0,0 +1,413 @@
|
||||||
|
import { BookingStatus } from "@prisma/client";
|
||||||
|
import type { Prisma } from "@prisma/client";
|
||||||
|
import type { NextApiRequest, NextApiResponse } from "next";
|
||||||
|
import * as z from "zod";
|
||||||
|
|
||||||
|
import EventManager from "@calcom/core/EventManager";
|
||||||
|
import { sendScheduledEmails } from "@calcom/emails";
|
||||||
|
import { getCalEventResponses } from "@calcom/features/bookings/lib/getCalEventResponses";
|
||||||
|
import { handleConfirmation } from "@calcom/features/bookings/lib/handleConfirmation";
|
||||||
|
import { isPrismaObjOrUndefined, parseRecurringEvent } from "@calcom/lib";
|
||||||
|
import { IS_PRODUCTION } from "@calcom/lib/constants";
|
||||||
|
import { getErrorFromUnknown } from "@calcom/lib/errors";
|
||||||
|
import { HttpError as HttpCode } from "@calcom/lib/http-error";
|
||||||
|
import { getTranslation } from "@calcom/lib/server/i18n";
|
||||||
|
import prisma, { bookingMinimalSelect } from "@calcom/prisma";
|
||||||
|
import type { CalendarEvent } from "@calcom/types/Calendar";
|
||||||
|
|
||||||
|
export const config = {
|
||||||
|
api: {
|
||||||
|
bodyParser: false,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
async function getEventType(id: number) {
|
||||||
|
return prisma.eventType.findUnique({
|
||||||
|
where: {
|
||||||
|
id,
|
||||||
|
},
|
||||||
|
select: {
|
||||||
|
recurringEvent: true,
|
||||||
|
requiresConfirmation: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getBooking(bookingId: number) {
|
||||||
|
const booking = await prisma.booking.findUnique({
|
||||||
|
where: {
|
||||||
|
id: bookingId,
|
||||||
|
},
|
||||||
|
select: {
|
||||||
|
...bookingMinimalSelect,
|
||||||
|
eventType: true,
|
||||||
|
smsReminderNumber: true,
|
||||||
|
location: true,
|
||||||
|
eventTypeId: true,
|
||||||
|
userId: true,
|
||||||
|
uid: true,
|
||||||
|
paid: true,
|
||||||
|
destinationCalendar: true,
|
||||||
|
status: true,
|
||||||
|
user: {
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
username: true,
|
||||||
|
credentials: true,
|
||||||
|
timeZone: true,
|
||||||
|
email: true,
|
||||||
|
name: true,
|
||||||
|
locale: true,
|
||||||
|
destinationCalendar: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!booking) throw new HttpCode({ statusCode: 204, message: "No booking found" });
|
||||||
|
|
||||||
|
type EventTypeRaw = Awaited<ReturnType<typeof getEventType>>;
|
||||||
|
let eventTypeRaw: EventTypeRaw | null = null;
|
||||||
|
if (booking.eventTypeId) {
|
||||||
|
eventTypeRaw = await getEventType(booking.eventTypeId);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { user } = booking;
|
||||||
|
|
||||||
|
if (!user) throw new HttpCode({ statusCode: 204, message: "No user found" });
|
||||||
|
|
||||||
|
const t = await getTranslation(user.locale ?? "en", "common");
|
||||||
|
const attendeesListPromises = booking.attendees.map(async (attendee) => {
|
||||||
|
return {
|
||||||
|
name: attendee.name,
|
||||||
|
email: attendee.email,
|
||||||
|
timeZone: attendee.timeZone,
|
||||||
|
language: {
|
||||||
|
translate: await getTranslation(attendee.locale ?? "en", "common"),
|
||||||
|
locale: attendee.locale ?? "en",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const attendeesList = await Promise.all(attendeesListPromises);
|
||||||
|
|
||||||
|
const evt: CalendarEvent = {
|
||||||
|
type: booking.title,
|
||||||
|
title: booking.title,
|
||||||
|
description: booking.description || undefined,
|
||||||
|
startTime: booking.startTime.toISOString(),
|
||||||
|
endTime: booking.endTime.toISOString(),
|
||||||
|
customInputs: isPrismaObjOrUndefined(booking.customInputs),
|
||||||
|
organizer: {
|
||||||
|
email: user.email,
|
||||||
|
name: user.name!,
|
||||||
|
timeZone: user.timeZone,
|
||||||
|
language: { translate: t, locale: user.locale ?? "en" },
|
||||||
|
id: user.id,
|
||||||
|
},
|
||||||
|
attendees: attendeesList,
|
||||||
|
uid: booking.uid,
|
||||||
|
destinationCalendar: booking.destinationCalendar || user.destinationCalendar,
|
||||||
|
recurringEvent: parseRecurringEvent(eventTypeRaw?.recurringEvent),
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
booking,
|
||||||
|
user,
|
||||||
|
evt,
|
||||||
|
eventTypeRaw,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function handlePaymentSuccess(payload: z.infer<typeof MPPaymentSchema>) {
|
||||||
|
const payment = await prisma.payment.findFirst({
|
||||||
|
where: {
|
||||||
|
externalId: payload.external_reference,
|
||||||
|
},
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
bookingId: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
if (!payment?.bookingId) {
|
||||||
|
console.log(JSON.stringify(payload), JSON.stringify(payment));
|
||||||
|
}
|
||||||
|
if (!payment?.bookingId) throw new HttpCode({ statusCode: 204, message: "Payment not found" });
|
||||||
|
|
||||||
|
const booking = await prisma.booking.findUnique({
|
||||||
|
where: {
|
||||||
|
id: payment.bookingId,
|
||||||
|
},
|
||||||
|
select: {
|
||||||
|
...bookingMinimalSelect,
|
||||||
|
eventType: true,
|
||||||
|
smsReminderNumber: true,
|
||||||
|
location: true,
|
||||||
|
eventTypeId: true,
|
||||||
|
userId: true,
|
||||||
|
uid: true,
|
||||||
|
paid: true,
|
||||||
|
destinationCalendar: true,
|
||||||
|
status: true,
|
||||||
|
responses: true,
|
||||||
|
user: {
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
username: true,
|
||||||
|
credentials: true,
|
||||||
|
timeZone: true,
|
||||||
|
email: true,
|
||||||
|
name: true,
|
||||||
|
locale: true,
|
||||||
|
destinationCalendar: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!booking) throw new HttpCode({ statusCode: 204, message: "No booking found" });
|
||||||
|
|
||||||
|
type EventTypeRaw = Awaited<ReturnType<typeof getEventType>>;
|
||||||
|
let eventTypeRaw: EventTypeRaw | null = null;
|
||||||
|
if (booking.eventTypeId) {
|
||||||
|
eventTypeRaw = await getEventType(booking.eventTypeId);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { user } = booking;
|
||||||
|
|
||||||
|
if (!user) throw new HttpCode({ statusCode: 204, message: "No user found" });
|
||||||
|
|
||||||
|
const t = await getTranslation(user.locale ?? "en", "common");
|
||||||
|
const attendeesListPromises = booking.attendees.map(async (attendee) => {
|
||||||
|
return {
|
||||||
|
name: attendee.name,
|
||||||
|
email: attendee.email,
|
||||||
|
timeZone: attendee.timeZone,
|
||||||
|
language: {
|
||||||
|
translate: await getTranslation(attendee.locale ?? "en", "common"),
|
||||||
|
locale: attendee.locale ?? "en",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const attendeesList = await Promise.all(attendeesListPromises);
|
||||||
|
|
||||||
|
const evt: CalendarEvent = {
|
||||||
|
type: booking.title,
|
||||||
|
title: booking.title,
|
||||||
|
description: booking.description || undefined,
|
||||||
|
startTime: booking.startTime.toISOString(),
|
||||||
|
endTime: booking.endTime.toISOString(),
|
||||||
|
customInputs: isPrismaObjOrUndefined(booking.customInputs),
|
||||||
|
...getCalEventResponses({
|
||||||
|
booking: booking,
|
||||||
|
bookingFields: booking.eventType?.bookingFields || null,
|
||||||
|
}),
|
||||||
|
organizer: {
|
||||||
|
email: user.email,
|
||||||
|
name: user.name!,
|
||||||
|
timeZone: user.timeZone,
|
||||||
|
language: { translate: t, locale: user.locale ?? "en" },
|
||||||
|
},
|
||||||
|
attendees: attendeesList,
|
||||||
|
uid: booking.uid,
|
||||||
|
destinationCalendar: booking.destinationCalendar || user.destinationCalendar,
|
||||||
|
recurringEvent: parseRecurringEvent(eventTypeRaw?.recurringEvent),
|
||||||
|
};
|
||||||
|
|
||||||
|
if (booking.location) evt.location = booking.location;
|
||||||
|
|
||||||
|
const bookingData: Prisma.BookingUpdateInput = {
|
||||||
|
paid: true,
|
||||||
|
status: BookingStatus.ACCEPTED,
|
||||||
|
};
|
||||||
|
|
||||||
|
const isConfirmed = booking.status === BookingStatus.ACCEPTED;
|
||||||
|
if (isConfirmed) {
|
||||||
|
const eventManager = new EventManager(user);
|
||||||
|
const scheduleResult = await eventManager.create(evt);
|
||||||
|
bookingData.references = { create: scheduleResult.referencesToCreate };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (eventTypeRaw?.requiresConfirmation) {
|
||||||
|
delete bookingData.status;
|
||||||
|
}
|
||||||
|
|
||||||
|
const paymentUpdate = prisma.payment.update({
|
||||||
|
where: {
|
||||||
|
id: payment.id,
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
success: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const bookingUpdate = prisma.booking.update({
|
||||||
|
where: {
|
||||||
|
id: booking.id,
|
||||||
|
},
|
||||||
|
data: bookingData,
|
||||||
|
});
|
||||||
|
|
||||||
|
await prisma.$transaction([paymentUpdate, bookingUpdate]);
|
||||||
|
|
||||||
|
if (!isConfirmed && !eventTypeRaw?.requiresConfirmation) {
|
||||||
|
await handleConfirmation({ user, evt, prisma, bookingId: booking.id, booking, paid: true });
|
||||||
|
} else {
|
||||||
|
await sendScheduledEmails({ ...evt });
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new HttpCode({
|
||||||
|
statusCode: 200,
|
||||||
|
message: `Booking with id '${booking.id}' was paid and confirmed.`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||||
|
try {
|
||||||
|
console.log("Webhook Received", req.method, req.body);
|
||||||
|
if (req.method !== "POST") {
|
||||||
|
throw new HttpCode({ statusCode: 405, message: "Method Not Allowed" });
|
||||||
|
}
|
||||||
|
const { body } = req;
|
||||||
|
// We first receive a notification from Mercado Pago that a payment was made
|
||||||
|
console.log("Webhook Received", body);
|
||||||
|
if (MercadoPagoPayloadSchemaEvent.safeParse(body).success || !!body.action) {
|
||||||
|
return res.status(200).end();
|
||||||
|
}
|
||||||
|
|
||||||
|
// The second notification is the actual payment
|
||||||
|
const parse = MPPaymentSchema.safeParse(body);
|
||||||
|
if (!parse.success) {
|
||||||
|
throw new HttpCode({ statusCode: 400, message: "Bad Request1" });
|
||||||
|
}
|
||||||
|
const { data: parsedPayload } = parse;
|
||||||
|
await handlePaymentSuccess(parsedPayload);
|
||||||
|
} catch (_err) {
|
||||||
|
const err = getErrorFromUnknown(_err);
|
||||||
|
console.error(`Webhook Error: ${err.message}`);
|
||||||
|
res.status(200).send({
|
||||||
|
message: err.message,
|
||||||
|
stack: IS_PRODUCTION ? undefined : err.stack,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return a response to acknowledge receipt of the event
|
||||||
|
res.status(200).end();
|
||||||
|
}
|
||||||
|
|
||||||
|
const MercadoPagoPayloadSchemaEvent = z.object({
|
||||||
|
action: z.string(),
|
||||||
|
api_version: z.string(),
|
||||||
|
application_id: z.string(),
|
||||||
|
date_created: z.string(),
|
||||||
|
id: z.string(),
|
||||||
|
live_mode: z.boolean(),
|
||||||
|
type: z.string(),
|
||||||
|
user_id: z.number(),
|
||||||
|
data: z.object({
|
||||||
|
id: z.string().optional(),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
const itemSchema = z.object({
|
||||||
|
id: z.string(),
|
||||||
|
title: z.string(),
|
||||||
|
description: z.string(),
|
||||||
|
picture_url: z.string(),
|
||||||
|
category_id: z.string(),
|
||||||
|
quantity: z.number(),
|
||||||
|
currency_id: z.string(),
|
||||||
|
unit_price: z.number(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const payerSchema = z.object({
|
||||||
|
email: z.string(),
|
||||||
|
identification: z
|
||||||
|
.object({
|
||||||
|
type: z.string().optional(),
|
||||||
|
number: z.string().optional(),
|
||||||
|
})
|
||||||
|
.optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const paymentMethodSchema = z.object({
|
||||||
|
installments: z.number(),
|
||||||
|
payment_type_id: z.string(),
|
||||||
|
issuer_id: z.string().optional(),
|
||||||
|
excluded_payment_methods: z
|
||||||
|
.array(
|
||||||
|
z.object({
|
||||||
|
id: z.string(),
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.optional(),
|
||||||
|
excluded_payment_types: z
|
||||||
|
.array(
|
||||||
|
z.object({
|
||||||
|
id: z.string(),
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const shippingSchema = z.object({
|
||||||
|
receiver_address: z
|
||||||
|
.object({
|
||||||
|
zip_code: z.string(),
|
||||||
|
street_name: z.string(),
|
||||||
|
street_number: z.string(),
|
||||||
|
floor: z.string().optional(),
|
||||||
|
apartment: z.string().optional(),
|
||||||
|
})
|
||||||
|
.optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const redirectUrlsSchema = z.object({
|
||||||
|
success: z.string(),
|
||||||
|
pending: z.string().optional(),
|
||||||
|
failure: z.string().optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const backUrlsSchema = z.object({
|
||||||
|
success: z.string().optional(),
|
||||||
|
pending: z.string().optional(),
|
||||||
|
failure: z.string().optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const MPPaymentSchema = z.object({
|
||||||
|
additional_info: z.string(),
|
||||||
|
auto_return: z.string(),
|
||||||
|
back_urls: backUrlsSchema,
|
||||||
|
binary_mode: z.boolean(),
|
||||||
|
client_id: z.string(),
|
||||||
|
collector_id: z.number(),
|
||||||
|
coupon_code: z.nullable(z.string()),
|
||||||
|
coupon_labels: z.nullable(z.array(z.string())),
|
||||||
|
date_created: z.string(),
|
||||||
|
date_of_expiration: z.nullable(z.string()),
|
||||||
|
expiration_date_from: z.nullable(z.string()),
|
||||||
|
expiration_date_to: z.nullable(z.string()),
|
||||||
|
expires: z.boolean(),
|
||||||
|
external_reference: z.string(),
|
||||||
|
id: z.string(),
|
||||||
|
init_point: z.string(),
|
||||||
|
internal_metadata: z.nullable(z.object({}).optional()),
|
||||||
|
items: z.array(itemSchema),
|
||||||
|
marketplace: z.string(),
|
||||||
|
marketplace_fee: z.number(),
|
||||||
|
metadata: z.record(z.unknown()),
|
||||||
|
notification_url: z.nullable(z.string()),
|
||||||
|
operation_type: z.string(),
|
||||||
|
payer: payerSchema,
|
||||||
|
payment_methods: paymentMethodSchema,
|
||||||
|
processing_modes: z.nullable(z.array(z.string())),
|
||||||
|
product_id: z.nullable(z.string()),
|
||||||
|
redirect_urls: redirectUrlsSchema,
|
||||||
|
sandbox_init_point: z.string(),
|
||||||
|
site_id: z.string(),
|
||||||
|
shipments: z.array(shippingSchema),
|
||||||
|
total_amount: z.nullable(z.number()),
|
||||||
|
last_updated: z.nullable(z.string()),
|
||||||
|
});
|
|
@ -0,0 +1,41 @@
|
||||||
|
import type { NextApiRequest, NextApiResponse } from "next";
|
||||||
|
|
||||||
|
import prisma from "@calcom/prisma";
|
||||||
|
|
||||||
|
import config from "../config.json";
|
||||||
|
|
||||||
|
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||||
|
if (!req.session?.user?.id) {
|
||||||
|
return res.status(401).json({ message: "You must be logged in to do this" });
|
||||||
|
}
|
||||||
|
const appType = config.type;
|
||||||
|
try {
|
||||||
|
const alreadyInstalled = await prisma.credential.findFirst({
|
||||||
|
where: {
|
||||||
|
type: appType,
|
||||||
|
userId: req.session.user.id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
if (alreadyInstalled) {
|
||||||
|
throw new Error("Already installed");
|
||||||
|
}
|
||||||
|
const installation = await prisma.credential.create({
|
||||||
|
data: {
|
||||||
|
type: appType,
|
||||||
|
key: {},
|
||||||
|
userId: req.session.user.id,
|
||||||
|
appId: "mercado_pago",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
if (!installation) {
|
||||||
|
throw new Error("Unable to create user credential for mercado pago");
|
||||||
|
}
|
||||||
|
} catch (error: unknown) {
|
||||||
|
if (error instanceof Error) {
|
||||||
|
return res.status(500).json({ message: error.message });
|
||||||
|
}
|
||||||
|
return res.status(500);
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.status(200).json({ url: "/apps/mercado_pago/setup" });
|
||||||
|
}
|
|
@ -0,0 +1,3 @@
|
||||||
|
export { default as add } from "./add";
|
||||||
|
export { default as payment } from "./payment";
|
||||||
|
export { default as webhook } from "./_webhook";
|
|
@ -0,0 +1,312 @@
|
||||||
|
import { BookingStatus } from "@prisma/client";
|
||||||
|
import type { Prisma } from "@prisma/client";
|
||||||
|
import * as MercadoPago from "mercadopago";
|
||||||
|
import type { NextApiRequest, NextApiResponse } from "next";
|
||||||
|
import * as z from "zod";
|
||||||
|
|
||||||
|
import { mercadoPagoCredentialKeysSchema } from "@calcom/app-store/mercado_pago/lib/PaymentService";
|
||||||
|
import EventManager from "@calcom/core/EventManager";
|
||||||
|
import { sendScheduledEmails } from "@calcom/emails";
|
||||||
|
import { getCalEventResponses } from "@calcom/features/bookings/lib/getCalEventResponses";
|
||||||
|
import { handleConfirmation } from "@calcom/features/bookings/lib/handleConfirmation";
|
||||||
|
import { isPrismaObjOrUndefined, parseRecurringEvent } from "@calcom/lib";
|
||||||
|
import { IS_PRODUCTION } from "@calcom/lib/constants";
|
||||||
|
import { getErrorFromUnknown } from "@calcom/lib/errors";
|
||||||
|
import { HttpError as HttpCode } from "@calcom/lib/http-error";
|
||||||
|
import { getTranslation } from "@calcom/lib/server/i18n";
|
||||||
|
import prisma, { bookingMinimalSelect } from "@calcom/prisma";
|
||||||
|
import type { CalendarEvent } from "@calcom/types/Calendar";
|
||||||
|
|
||||||
|
export const config = {
|
||||||
|
api: {
|
||||||
|
bodyParser: false,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
async function getEventType(id: number) {
|
||||||
|
return prisma.eventType.findUnique({
|
||||||
|
where: {
|
||||||
|
id,
|
||||||
|
},
|
||||||
|
select: {
|
||||||
|
recurringEvent: true,
|
||||||
|
requiresConfirmation: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function handlePaymentSuccess(payload: z.infer<typeof paymentSchema>) {
|
||||||
|
const payment = await prisma.payment.findFirst({
|
||||||
|
where: {
|
||||||
|
uid: payload.external_reference,
|
||||||
|
},
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
bookingId: true,
|
||||||
|
success: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!payment?.bookingId) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
message: "No payment found",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (payment.success) {
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
message: "Payment already processed",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const booking = await prisma.booking.findUnique({
|
||||||
|
where: {
|
||||||
|
id: payment.bookingId,
|
||||||
|
},
|
||||||
|
select: {
|
||||||
|
...bookingMinimalSelect,
|
||||||
|
eventType: true,
|
||||||
|
smsReminderNumber: true,
|
||||||
|
location: true,
|
||||||
|
eventTypeId: true,
|
||||||
|
userId: true,
|
||||||
|
uid: true,
|
||||||
|
paid: true,
|
||||||
|
destinationCalendar: true,
|
||||||
|
status: true,
|
||||||
|
responses: true,
|
||||||
|
user: {
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
username: true,
|
||||||
|
timeZone: true,
|
||||||
|
email: true,
|
||||||
|
name: true,
|
||||||
|
locale: true,
|
||||||
|
destinationCalendar: true,
|
||||||
|
credentials: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!booking) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
message: "No booking found",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
type EventTypeRaw = Awaited<ReturnType<typeof getEventType>>;
|
||||||
|
let eventTypeRaw: EventTypeRaw | null = null;
|
||||||
|
if (booking.eventTypeId) {
|
||||||
|
eventTypeRaw = await getEventType(booking.eventTypeId);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { user } = booking;
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
message: "No user found",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load MP app credentials from user
|
||||||
|
const mpCredential = user.credentials.find((cred) => cred.appId === "mercado_pago");
|
||||||
|
|
||||||
|
if (!mpCredential) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
message: "No Mercado Pago credentials found",
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
const safeParseKey = mercadoPagoCredentialKeysSchema.safeParse(mpCredential.key);
|
||||||
|
if (!safeParseKey.success) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
message: "Invalid Mercado Pago credentials",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
// Validate payment using MercadoPago API
|
||||||
|
MercadoPago.configure({
|
||||||
|
access_token: safeParseKey.data.access_token,
|
||||||
|
});
|
||||||
|
const paymentInfo = await MercadoPago.payment.get(payload.payment_id);
|
||||||
|
|
||||||
|
const safeParsePaymentId = getPaymentIdSchema.safeParse(paymentInfo.body);
|
||||||
|
|
||||||
|
if (
|
||||||
|
!safeParsePaymentId.success ||
|
||||||
|
(safeParsePaymentId.success && safeParsePaymentId.data.status !== "approved")
|
||||||
|
) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
message: "Payment not approved",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const t = await getTranslation(user.locale ?? "en", "common");
|
||||||
|
const attendeesListPromises = booking.attendees.map(async (attendee) => {
|
||||||
|
return {
|
||||||
|
name: attendee.name,
|
||||||
|
email: attendee.email,
|
||||||
|
timeZone: attendee.timeZone,
|
||||||
|
language: {
|
||||||
|
translate: await getTranslation(attendee.locale ?? "en", "common"),
|
||||||
|
locale: attendee.locale ?? "en",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const attendeesList = await Promise.all(attendeesListPromises);
|
||||||
|
|
||||||
|
const evt: CalendarEvent = {
|
||||||
|
type: booking.title,
|
||||||
|
title: booking.title,
|
||||||
|
description: booking.description || undefined,
|
||||||
|
startTime: booking.startTime.toISOString(),
|
||||||
|
endTime: booking.endTime.toISOString(),
|
||||||
|
customInputs: isPrismaObjOrUndefined(booking.customInputs),
|
||||||
|
...getCalEventResponses({
|
||||||
|
booking: booking,
|
||||||
|
bookingFields: booking.eventType?.bookingFields || null,
|
||||||
|
}),
|
||||||
|
organizer: {
|
||||||
|
email: user.email,
|
||||||
|
name: user.name!,
|
||||||
|
timeZone: user.timeZone,
|
||||||
|
language: { translate: t, locale: user.locale ?? "en" },
|
||||||
|
},
|
||||||
|
attendees: attendeesList,
|
||||||
|
uid: booking.uid,
|
||||||
|
destinationCalendar: booking.destinationCalendar || user.destinationCalendar,
|
||||||
|
recurringEvent: parseRecurringEvent(eventTypeRaw?.recurringEvent),
|
||||||
|
};
|
||||||
|
|
||||||
|
if (booking.location) evt.location = booking.location;
|
||||||
|
|
||||||
|
const bookingData: Prisma.BookingUpdateInput = {
|
||||||
|
paid: true,
|
||||||
|
status: BookingStatus.ACCEPTED,
|
||||||
|
};
|
||||||
|
|
||||||
|
const isConfirmed = booking.status === BookingStatus.ACCEPTED;
|
||||||
|
if (isConfirmed) {
|
||||||
|
const eventManager = new EventManager(user);
|
||||||
|
const scheduleResult = await eventManager.create(evt);
|
||||||
|
bookingData.references = { create: scheduleResult.referencesToCreate };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (eventTypeRaw?.requiresConfirmation) {
|
||||||
|
delete bookingData.status;
|
||||||
|
}
|
||||||
|
|
||||||
|
const paymentUpdate = prisma.payment.update({
|
||||||
|
where: {
|
||||||
|
id: payment.id,
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
success: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const bookingUpdate = prisma.booking.update({
|
||||||
|
where: {
|
||||||
|
id: booking.id,
|
||||||
|
},
|
||||||
|
data: bookingData,
|
||||||
|
});
|
||||||
|
|
||||||
|
await prisma.$transaction([paymentUpdate, bookingUpdate]);
|
||||||
|
|
||||||
|
if (!isConfirmed && !eventTypeRaw?.requiresConfirmation) {
|
||||||
|
await handleConfirmation({ user, evt, prisma, bookingId: booking.id, booking, paid: true });
|
||||||
|
} else {
|
||||||
|
await sendScheduledEmails({ ...evt });
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
message: "Payment success",
|
||||||
|
data: {
|
||||||
|
bookingUid: booking.uid,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||||
|
try {
|
||||||
|
if (req.method !== "GET") {
|
||||||
|
throw new HttpCode({ statusCode: 405, message: "Method Not Allowed" });
|
||||||
|
}
|
||||||
|
const { query } = req;
|
||||||
|
const parse = paymentSchema.safeParse(query);
|
||||||
|
if (!parse.success) {
|
||||||
|
console.error("Error while parsing query: ", parse.error);
|
||||||
|
throw new HttpCode({ statusCode: 200, message: "Bad Request" });
|
||||||
|
}
|
||||||
|
const { data: parsedPayload } = parse;
|
||||||
|
const result = await handlePaymentSuccess(parsedPayload);
|
||||||
|
if (!result.success) {
|
||||||
|
throw new HttpCode({ statusCode: 200, message: result.message });
|
||||||
|
}
|
||||||
|
return res.redirect(`/booking/${result.data?.bookingUid}`);
|
||||||
|
} catch (_err) {
|
||||||
|
const err = getErrorFromUnknown(_err);
|
||||||
|
console.error(`Payment Error: ${err.message}`);
|
||||||
|
res.status(200).send({
|
||||||
|
message: err.message,
|
||||||
|
stack: IS_PRODUCTION ? undefined : err.stack,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const paymentSchema = z.object({
|
||||||
|
redirect_status: z.enum(["success", "pending", "failure"]),
|
||||||
|
collection_id: z.string(),
|
||||||
|
collection_status: z.string(),
|
||||||
|
payment_id: z.coerce.number(),
|
||||||
|
status: z.string(),
|
||||||
|
external_reference: z.string(),
|
||||||
|
payment_type: z.string(),
|
||||||
|
merchant_order_id: z.string(),
|
||||||
|
preference_id: z.string(),
|
||||||
|
site_id: z.string(),
|
||||||
|
processing_mode: z.string(),
|
||||||
|
merchant_account_id: z.string().nullable(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const payerSchema = z.object({});
|
||||||
|
|
||||||
|
const transactionDetailsSchema = z.object({});
|
||||||
|
|
||||||
|
const getPaymentIdSchema = z
|
||||||
|
.object({
|
||||||
|
id: z.number(),
|
||||||
|
date_created: z.string(),
|
||||||
|
date_approved: z.string(),
|
||||||
|
date_last_updated: z.string(),
|
||||||
|
money_release_date: z.string(),
|
||||||
|
payment_method_id: z.string(),
|
||||||
|
payment_type_id: z.string(),
|
||||||
|
status: z.string(),
|
||||||
|
status_detail: z.string(),
|
||||||
|
currency_id: z.string(),
|
||||||
|
description: z.string(),
|
||||||
|
collector_id: z.number(),
|
||||||
|
payer: payerSchema,
|
||||||
|
metadata: z.object({}).optional(),
|
||||||
|
additional_info: z.object({}).optional(),
|
||||||
|
transaction_amount: z.number(),
|
||||||
|
transaction_amount_refunded: z.number(),
|
||||||
|
coupon_amount: z.number(),
|
||||||
|
transaction_details: transactionDetailsSchema,
|
||||||
|
installments: z.number(),
|
||||||
|
card: z.object({}).optional(),
|
||||||
|
})
|
||||||
|
.passthrough();
|
|
@ -0,0 +1,67 @@
|
||||||
|
import { useRouter } from "next/router";
|
||||||
|
import { useState } from "react";
|
||||||
|
|
||||||
|
import { useAppContextWithSchema } from "@calcom/app-store/EventTypeAppContext";
|
||||||
|
import AppCard from "@calcom/app-store/_components/AppCard";
|
||||||
|
import type { EventTypeAppCardComponent } from "@calcom/app-store/types";
|
||||||
|
import { WEBAPP_URL } from "@calcom/lib/constants";
|
||||||
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
|
import { Alert, TextField } from "@calcom/ui";
|
||||||
|
|
||||||
|
import type { appDataSchema } from "../zod";
|
||||||
|
import type { Prisma } from ".prisma/client";
|
||||||
|
|
||||||
|
const EventTypeAppCard: EventTypeAppCardComponent = function EventTypeAppCard({ app, eventType }) {
|
||||||
|
const { asPath } = useRouter();
|
||||||
|
const [getAppData, setAppData] = useAppContextWithSchema<typeof appDataSchema>();
|
||||||
|
const price = getAppData("price");
|
||||||
|
const [requirePayment, setRequirePayment] = useState(getAppData("enabled"));
|
||||||
|
const { t } = useLocale();
|
||||||
|
const recurringEventDefined = eventType.recurringEvent?.count !== undefined;
|
||||||
|
|
||||||
|
// Get the app data from the context
|
||||||
|
const credential = app.credentials.find((c) => c.key === "currency") || { key: { currency: "MXN" } };
|
||||||
|
const currency = (credential?.key as Prisma.JsonObject).currency as string;
|
||||||
|
return (
|
||||||
|
<AppCard
|
||||||
|
returnTo={WEBAPP_URL + asPath}
|
||||||
|
setAppData={setAppData}
|
||||||
|
app={app}
|
||||||
|
switchChecked={requirePayment}
|
||||||
|
switchOnClick={(enabled) => {
|
||||||
|
setRequirePayment(enabled);
|
||||||
|
}}
|
||||||
|
description={<>Demo {currency}</>}>
|
||||||
|
<>
|
||||||
|
{recurringEventDefined ? (
|
||||||
|
<Alert className="mt-2" severity="warning" title={t("warning_recurring_event_payment")} />
|
||||||
|
) : (
|
||||||
|
requirePayment && (
|
||||||
|
<div className="mt-2 block items-center sm:flex">
|
||||||
|
<TextField
|
||||||
|
label=""
|
||||||
|
addOnLeading="$"
|
||||||
|
addOnSuffix={currency || "MXN"}
|
||||||
|
step="0.01"
|
||||||
|
min="0.5"
|
||||||
|
type="number"
|
||||||
|
required
|
||||||
|
className="block w-full rounded-sm border-gray-300 pl-2 pr-12 text-sm"
|
||||||
|
placeholder="Price"
|
||||||
|
onChange={(e) => {
|
||||||
|
setAppData("price", Number(e.target.value) * 100);
|
||||||
|
if (currency) {
|
||||||
|
setAppData("currency", currency);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
value={price > 0 ? price / 100 : undefined}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
</AppCard>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default EventTypeAppCard;
|
|
@ -0,0 +1,21 @@
|
||||||
|
import type { IMercadoPagoPaymentComponentProps } from "mercado_pago/lib/interfaces";
|
||||||
|
import Link from "next/link";
|
||||||
|
|
||||||
|
import { IS_PRODUCTION } from "@calcom/lib/constants";
|
||||||
|
|
||||||
|
export const MercadoPagoPaymentComponent = (props: IMercadoPagoPaymentComponentProps) => {
|
||||||
|
const { payment, eventType, user, location, bookingId, bookingUid } = props;
|
||||||
|
const paymentInitLink = IS_PRODUCTION ? payment.data.init_point : payment.data.sandbox_init_point;
|
||||||
|
console.log({ paymentInitLink });
|
||||||
|
return (
|
||||||
|
<div className="flex h-full w-full flex-col items-center justify-center">
|
||||||
|
<Link
|
||||||
|
href={paymentInitLink}
|
||||||
|
className="inline-flex items-center justify-center rounded-md border border-transparent bg-white px-4 py-2 text-base font-medium text-black shadow-sm
|
||||||
|
hover:bg-gray-200 focus:outline-none focus:ring-2 focus:ring-[#009EE3] focus:ring-offset-2">
|
||||||
|
<img src="/api/app-store/mercado_pago/icon.png" alt="Mercado Pago" className="mr-2 w-20" />
|
||||||
|
<span>Pagar con Mercado Pago</span>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
|
@ -0,0 +1,17 @@
|
||||||
|
{
|
||||||
|
"/*": "Don't modify slug - If required, do it using cli edit command",
|
||||||
|
"name": "Mercado Pago",
|
||||||
|
"slug": "mercado_pago",
|
||||||
|
"type": "mercado_pago_payment",
|
||||||
|
"imageSrc": "/api/app-store/mercado_pago/icon.svg",
|
||||||
|
"logo": "/api/app-store/mercado_pago/icon.svg",
|
||||||
|
"url": "https://cal.com/apps/mercado_pago",
|
||||||
|
"variant": "payment",
|
||||||
|
"categories": ["payment"],
|
||||||
|
"publisher": "Cal.com",
|
||||||
|
"email": "support@cal.com",
|
||||||
|
"description": ".",
|
||||||
|
"extendsFeature": "EventType",
|
||||||
|
"__createdUsingCli": true,
|
||||||
|
"dirName": "mercadopagopayment"
|
||||||
|
}
|
|
@ -0,0 +1,3 @@
|
||||||
|
export * as api from "./api";
|
||||||
|
export * as lib from "./lib";
|
||||||
|
export { metadata } from "./_metadata";
|
|
@ -0,0 +1,152 @@
|
||||||
|
import type { Booking, Payment, PaymentOption, Prisma } from "@prisma/client";
|
||||||
|
import * as MercadoPago from "mercadopago";
|
||||||
|
import { v4 as uuidv4 } from "uuid";
|
||||||
|
import z from "zod";
|
||||||
|
|
||||||
|
import type { IAbstractPaymentService } from "@calcom/lib/PaymentService";
|
||||||
|
import prisma from "@calcom/prisma";
|
||||||
|
import type { CalendarEvent } from "@calcom/types/Calendar";
|
||||||
|
|
||||||
|
export const mercadoPagoCredentialKeysSchema = z.object({
|
||||||
|
access_token: z.string(),
|
||||||
|
public_key: z.string(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export class PaymentService implements IAbstractPaymentService {
|
||||||
|
private mercadoPago: typeof MercadoPago;
|
||||||
|
private credentials: z.infer<typeof mercadoPagoCredentialKeysSchema>;
|
||||||
|
|
||||||
|
constructor(credentials: { key: Prisma.JsonValue }) {
|
||||||
|
this.credentials = mercadoPagoCredentialKeysSchema.parse(credentials.key);
|
||||||
|
|
||||||
|
this.mercadoPago = MercadoPago;
|
||||||
|
}
|
||||||
|
|
||||||
|
async create(
|
||||||
|
payment: Pick<Prisma.PaymentUncheckedCreateInput, "amount" | "currency">,
|
||||||
|
bookingId: Booking["id"]
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
this.mercadoPago.configure({
|
||||||
|
access_token: this.credentials.access_token,
|
||||||
|
});
|
||||||
|
|
||||||
|
const booking = await prisma?.booking.findFirst({
|
||||||
|
select: {
|
||||||
|
uid: true,
|
||||||
|
title: true,
|
||||||
|
},
|
||||||
|
where: {
|
||||||
|
id: bookingId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
if (!booking) {
|
||||||
|
throw new Error();
|
||||||
|
}
|
||||||
|
const { title } = booking;
|
||||||
|
|
||||||
|
const paymentData = await prisma?.payment.create({
|
||||||
|
data: {
|
||||||
|
uid: uuidv4(),
|
||||||
|
app: {
|
||||||
|
connect: {
|
||||||
|
slug: "mercado_pago",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
booking: {
|
||||||
|
connect: {
|
||||||
|
id: bookingId,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
amount: payment.amount,
|
||||||
|
currency: payment.currency,
|
||||||
|
externalId: "",
|
||||||
|
data: {},
|
||||||
|
fee: 0,
|
||||||
|
refunded: false,
|
||||||
|
success: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const preference = await this.mercadoPago.preferences.create({
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
title,
|
||||||
|
unit_price: payment.amount / 100,
|
||||||
|
quantity: 1,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
back_urls: {
|
||||||
|
success: `${process.env.NEXT_PUBLIC_WEBAPP_URL}}api/integrations/mercado_pago/payment?redirect_status=success`,
|
||||||
|
failure: `${process.env.NEXT_PUBLIC_WEBAPP_URL}}api/integrations/mercado_pago/payment?redirect_status=failure`,
|
||||||
|
pending: `${process.env.NEXT_PUBLIC_WEBAPP_URL}}api/integrations/mercado_pago/payment?redirect_status=pending`,
|
||||||
|
},
|
||||||
|
auto_return: "approved",
|
||||||
|
external_reference: paymentData.uid,
|
||||||
|
});
|
||||||
|
|
||||||
|
await prisma?.payment.update({
|
||||||
|
where: {
|
||||||
|
id: paymentData.id,
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
externalId: preference?.body.id,
|
||||||
|
data: Object.assign({}, preference.body, {
|
||||||
|
mp_public_key: this.credentials.public_key,
|
||||||
|
mp_access_token: this.credentials.access_token,
|
||||||
|
}) as unknown as Prisma.InputJsonValue,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!paymentData) {
|
||||||
|
throw new Error();
|
||||||
|
}
|
||||||
|
return paymentData;
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
throw new Error("Payment could not be created");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
async update(): Promise<Payment> {
|
||||||
|
throw new Error("Method not implemented.");
|
||||||
|
}
|
||||||
|
async refund(): Promise<Payment> {
|
||||||
|
throw new Error("Method not implemented.");
|
||||||
|
}
|
||||||
|
|
||||||
|
collectCard(
|
||||||
|
payment: Pick<Prisma.PaymentUncheckedCreateInput, "amount" | "currency">,
|
||||||
|
bookingId: number,
|
||||||
|
bookerEmail: string,
|
||||||
|
paymentOption: PaymentOption
|
||||||
|
): Promise<Payment> {
|
||||||
|
throw new Error("Method not implemented.");
|
||||||
|
}
|
||||||
|
chargeCard(
|
||||||
|
payment: Pick<Prisma.PaymentUncheckedCreateInput, "amount" | "currency">,
|
||||||
|
bookingId: number
|
||||||
|
): Promise<Payment> {
|
||||||
|
throw new Error("Method not implemented.");
|
||||||
|
}
|
||||||
|
getPaymentPaidStatus(): Promise<string> {
|
||||||
|
throw new Error("Method not implemented.");
|
||||||
|
}
|
||||||
|
getPaymentDetails(): Promise<Payment> {
|
||||||
|
throw new Error("Method not implemented.");
|
||||||
|
}
|
||||||
|
afterPayment(
|
||||||
|
event: CalendarEvent,
|
||||||
|
booking: {
|
||||||
|
user: { email: string | null; name: string | null; timeZone: string } | null;
|
||||||
|
id: number;
|
||||||
|
startTime: { toISOString: () => string };
|
||||||
|
uid: string;
|
||||||
|
},
|
||||||
|
paymentData: Payment
|
||||||
|
): Promise<void> {
|
||||||
|
throw new Error("Method not implemented.");
|
||||||
|
}
|
||||||
|
deletePayment(paymentId: number): Promise<boolean> {
|
||||||
|
throw new Error("Method not implemented.");
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1 @@
|
||||||
|
export * from "./PaymentService";
|
|
@ -0,0 +1,79 @@
|
||||||
|
export interface IMercadoPagoPaymentComponentProps {
|
||||||
|
payment: {
|
||||||
|
amount: number;
|
||||||
|
appId: string;
|
||||||
|
bookingId: number;
|
||||||
|
currency: string;
|
||||||
|
data: MercadoPagoPaymentResponse;
|
||||||
|
paymentOption: string;
|
||||||
|
refunded: boolean;
|
||||||
|
success: boolean;
|
||||||
|
uid: string;
|
||||||
|
};
|
||||||
|
eventType: EventType;
|
||||||
|
user: User;
|
||||||
|
location?: string | null;
|
||||||
|
bookingId: number;
|
||||||
|
bookingUid: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface MercadoPagoPaymentResponse {
|
||||||
|
additional_info: string;
|
||||||
|
auto_return: string;
|
||||||
|
back_urls: {
|
||||||
|
failure: string;
|
||||||
|
pending: string;
|
||||||
|
success: string;
|
||||||
|
};
|
||||||
|
binary_mode: boolean;
|
||||||
|
client_id: string;
|
||||||
|
collector_id: number;
|
||||||
|
coupon_code: null;
|
||||||
|
coupon_labels: null;
|
||||||
|
date_created: string;
|
||||||
|
date_of_expiration: null;
|
||||||
|
expiration_date_from: null;
|
||||||
|
expiration_date_to: null;
|
||||||
|
expires: boolean;
|
||||||
|
external_reference: string;
|
||||||
|
id: string;
|
||||||
|
init_point: string;
|
||||||
|
internal_metadata: null;
|
||||||
|
items: object[];
|
||||||
|
last_updated: null;
|
||||||
|
marketplace: string;
|
||||||
|
marketplace_fee: number;
|
||||||
|
metadata: object;
|
||||||
|
notification_url: null;
|
||||||
|
operation_type: string;
|
||||||
|
payer: {
|
||||||
|
name: string;
|
||||||
|
email: string;
|
||||||
|
phone: object;
|
||||||
|
address: object;
|
||||||
|
surname: string;
|
||||||
|
};
|
||||||
|
payment_methods: {
|
||||||
|
installments: null;
|
||||||
|
default_card_id: null;
|
||||||
|
default_installments: null;
|
||||||
|
excluded_payment_types: string[];
|
||||||
|
excluded_payment_methods: string[];
|
||||||
|
};
|
||||||
|
processing_modes: null;
|
||||||
|
product_id: null;
|
||||||
|
redirect_urls: {
|
||||||
|
failure: string;
|
||||||
|
pending: string;
|
||||||
|
success: string;
|
||||||
|
};
|
||||||
|
sandbox_init_point: string;
|
||||||
|
shipments: {
|
||||||
|
receiver_address: object;
|
||||||
|
default_shipping_method: null;
|
||||||
|
};
|
||||||
|
site_id: string;
|
||||||
|
stripeAccount: string;
|
||||||
|
stripe_publishable_key: string;
|
||||||
|
total_amount: null;
|
||||||
|
}
|
|
@ -0,0 +1,14 @@
|
||||||
|
{
|
||||||
|
"$schema": "https://json.schemastore.org/package.json",
|
||||||
|
"private": true,
|
||||||
|
"name": "@calcom/mercado_pago",
|
||||||
|
"version": "0.0.0",
|
||||||
|
"main": "./index.ts",
|
||||||
|
"description": ".",
|
||||||
|
"dependencies": {
|
||||||
|
"@calcom/lib": "*"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@calcom/types": "*"
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,22 @@
|
||||||
|
import type { GetStaticPropsContext } from "next";
|
||||||
|
|
||||||
|
import getAppKeysFromSlug from "../../../_utils/getAppKeysFromSlug";
|
||||||
|
|
||||||
|
export const getStaticProps = async (ctx: GetStaticPropsContext) => {
|
||||||
|
console.log("props");
|
||||||
|
if (typeof ctx.params?.slug !== "string") return { notFound: true } as const;
|
||||||
|
let publicKey = "";
|
||||||
|
let accessToken = "";
|
||||||
|
const appKeys = await getAppKeysFromSlug("mercado_pago");
|
||||||
|
if (typeof appKeys.public_key === "string" && typeof appKeys.access_token === "string") {
|
||||||
|
publicKey = appKeys.public_key;
|
||||||
|
accessToken = appKeys.access_token;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
props: {
|
||||||
|
publicKey,
|
||||||
|
accessToken,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
|
@ -0,0 +1,192 @@
|
||||||
|
import Link from "next/link";
|
||||||
|
import { useRouter } from "next/router";
|
||||||
|
import { useState } from "react";
|
||||||
|
import { Toaster } from "react-hot-toast";
|
||||||
|
|
||||||
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
|
import { trpc } from "@calcom/trpc";
|
||||||
|
import { Button, showToast } from "@calcom/ui";
|
||||||
|
import { Select } from "@calcom/ui";
|
||||||
|
|
||||||
|
export interface IMercadoPagoSetupProps {
|
||||||
|
public_key: string;
|
||||||
|
access_token: string;
|
||||||
|
currency: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function MercadoPagoSetup(props: IMercadoPagoSetupProps) {
|
||||||
|
const [newPublicKey, setNewPublicKey] = useState("");
|
||||||
|
const [newAccessToken, setNewAccessToken] = useState("");
|
||||||
|
const [selectedCurrency, setSelectedCurrency] = useState({ label: "", value: "" });
|
||||||
|
const router = useRouter();
|
||||||
|
const { t } = useLocale();
|
||||||
|
|
||||||
|
const integrations = trpc.viewer.integrations.useQuery({ variant: "payment" });
|
||||||
|
|
||||||
|
const mercadoPagoCredentials: { credentialIds: number[] } | undefined = integrations.data?.items.find(
|
||||||
|
(item: { type: string }) => item.type === "mercado_pago_payment"
|
||||||
|
);
|
||||||
|
|
||||||
|
const [credentialId] = mercadoPagoCredentials?.credentialIds || [false];
|
||||||
|
const showContent = !!integrations.data && integrations.isSuccess && !!credentialId;
|
||||||
|
const saveKeysMutation = trpc.viewer.appsRouter.updateAppCredentials.useMutation({
|
||||||
|
onSuccess: () => {
|
||||||
|
showToast(t("keys_have_been_saved"), "success");
|
||||||
|
router.push("/event-types");
|
||||||
|
},
|
||||||
|
onError: (error) => {
|
||||||
|
showToast(error.message, "error");
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const saveKeys = async (key: {
|
||||||
|
publicKey: string;
|
||||||
|
accessToken: string;
|
||||||
|
currency: { label: string; value: string };
|
||||||
|
}) => {
|
||||||
|
if (typeof credentialId !== "number") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
saveKeysMutation.mutate({
|
||||||
|
credentialId,
|
||||||
|
key: {
|
||||||
|
public_key: key.publicKey,
|
||||||
|
access_token: key.accessToken,
|
||||||
|
currency: key.currency.value,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
if (integrations.isLoading) {
|
||||||
|
return <div className="absolute z-50 flex h-screen w-full items-center bg-gray-200" />;
|
||||||
|
}
|
||||||
|
|
||||||
|
const currencyOptions = [
|
||||||
|
{ label: "MXN Peso Mexicano", value: "MXN" },
|
||||||
|
{ label: "CLP Peso Chileno", value: "CLP" },
|
||||||
|
{ label: "ARS Peso Argentino", value: "ARS" },
|
||||||
|
{ label: "BRL Real Brasilero", value: "BRL" },
|
||||||
|
{ label: "PEN Sol Peruano", value: "PEN" },
|
||||||
|
{ label: "COP Peso Colombiano", value: "COP" },
|
||||||
|
{ label: "UY Peso Uruguayo", value: "UYU" },
|
||||||
|
{ label: "VES Bolivar Venezolano", value: "VES" },
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="dark:bg-default flex h-screen bg-gray-300">
|
||||||
|
{showContent ? (
|
||||||
|
<div className="bg-default m-auto max-w-[43em] overflow-auto rounded border border-gray-200 pb-10 md:p-10">
|
||||||
|
<div className="ml-2 ltr:mr-2 rtl:ml-2 md:ml-5">
|
||||||
|
<div className="invisible md:visible">
|
||||||
|
<img className="h-11" src="/api/app-store/mercado_pago/icon.svg" alt="Mercado Pago Logo" />
|
||||||
|
<p className="text-default text-lg">Mercado Pago</p>
|
||||||
|
</div>
|
||||||
|
<form autoComplete="off">
|
||||||
|
<div className="mt-5">
|
||||||
|
<label className="text-default block text-sm font-medium" htmlFor="public_key">
|
||||||
|
Public key
|
||||||
|
</label>
|
||||||
|
<div className="mt-1">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
name="public_key"
|
||||||
|
id="public_key"
|
||||||
|
className="text-default bg-default block w-full rounded-md border-gray-300 text-black shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm"
|
||||||
|
value={newPublicKey}
|
||||||
|
autoComplete="new-password"
|
||||||
|
onChange={(e) => setNewPublicKey(e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="mt-5">
|
||||||
|
<label className="text-default block text-sm font-medium" htmlFor="access_token">
|
||||||
|
{t("access_token")}
|
||||||
|
</label>
|
||||||
|
<div className="mt-1">
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
name="access_token"
|
||||||
|
id="access_token"
|
||||||
|
className="bg-default text-default block w-full rounded-md border-gray-300 text-black shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm"
|
||||||
|
value={newAccessToken}
|
||||||
|
autoComplete="new-password"
|
||||||
|
onChange={(e) => setNewAccessToken(e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-5">
|
||||||
|
<label className="text-default block text-sm font-medium" htmlFor="currency">
|
||||||
|
{t("currency")}
|
||||||
|
</label>
|
||||||
|
<Select
|
||||||
|
variant="default"
|
||||||
|
options={currencyOptions}
|
||||||
|
value={selectedCurrency}
|
||||||
|
className="text-black"
|
||||||
|
defaultValue={selectedCurrency}
|
||||||
|
onChange={(e) => {
|
||||||
|
if (e) {
|
||||||
|
setSelectedCurrency(e);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/* Button to submit */}
|
||||||
|
<div className="mt-5 flex flex-row justify-end">
|
||||||
|
<Button
|
||||||
|
color="secondary"
|
||||||
|
onClick={() =>
|
||||||
|
saveKeys({
|
||||||
|
publicKey: newPublicKey,
|
||||||
|
accessToken: newAccessToken,
|
||||||
|
currency: selectedCurrency,
|
||||||
|
})
|
||||||
|
}>
|
||||||
|
{t("save")}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
<div>
|
||||||
|
<p className="text-lgf text-default mt-5 font-bold">Setup instructions</p>
|
||||||
|
|
||||||
|
<ol className="text-default ml-1 list-decimal pl-2">
|
||||||
|
{/* @TODO: translate */}
|
||||||
|
<li>
|
||||||
|
Log into your Mercado Pago account and create a new app{" "}
|
||||||
|
<a
|
||||||
|
href="https://www.mercadopago.com.mx/developers/panel"
|
||||||
|
className="text-orange-600 underline">
|
||||||
|
{t("here")}
|
||||||
|
</a>
|
||||||
|
.
|
||||||
|
</li>
|
||||||
|
<li>Choose a name for your application.</li>
|
||||||
|
<li>Select Online payments solution.</li>
|
||||||
|
<li>Choose No for Using online platform.</li>
|
||||||
|
<li>CheckoutAPI as integration product.</li>
|
||||||
|
<li>Accept terms and Create APP</li>
|
||||||
|
<li>Go back to dashboard, click on new app and copy the credentials.</li>
|
||||||
|
<li>Paste them on the required field and save them.</li>
|
||||||
|
<li>You're all setup</li>
|
||||||
|
</ol>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="mt-5 ml-5">
|
||||||
|
<div>Mercado Pago</div>
|
||||||
|
<div className="mt-3">
|
||||||
|
<Link href="/apps/mercado_pago" passHref={true} legacyBehavior>
|
||||||
|
<Button>{t("go_to_app_store")}</Button>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<Toaster position="bottom-right" />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
Binary file not shown.
After Width: | Height: | Size: 365 KiB |
Binary file not shown.
After Width: | Height: | Size: 394 KiB |
Binary file not shown.
After Width: | Height: | Size: 394 KiB |
Binary file not shown.
After Width: | Height: | Size: 64 KiB |
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 6.9 KiB |
|
@ -0,0 +1,57 @@
|
||||||
|
ID,Date,Transaction Type,Currency,Amount,Client,Contract Name,Contract URL,Invoice URL,Invoice Description,Withdraw Method,Withdraw Account Holder Name,Withdraw Account Number
|
||||||
|
28407799,2023-01-17 01:52:38,pay_out,USD,-1200.00,,,,,,bank_transfer,Alan Esteban Castro Ayala,
|
||||||
|
28048891,2023-01-02 09:16:39,pay_out,USD,-600.00,,,,,,bank_transfer,Alan Esteban Castro Ayala,
|
||||||
|
28006521,2022-12-31 12:16:23,pay_out,USD,-800.00,,,,,,bank_transfer,Alan Esteban Castro Ayala,
|
||||||
|
27895486,2022-12-28 01:10:11,pay_out,USD,-1000.00,,,,,,bank_transfer,Alan Esteban Castro Ayala,
|
||||||
|
27762420,2022-12-22 06:05:58,pay_out,USD,-1250.00,,,,,,bank_transfer,Alan Esteban Castro Ayala,
|
||||||
|
27622966,2022-12-19 06:02:08,pay_in,USD,6667.00,Peer Oke Richelsen (peer@cal.com),Alan Castro,https://app.deel.com/contract/n2x3nqnej9mj,https://app.deel.com/invoice/5736722,,,,
|
||||||
|
27606027,2022-12-19 11:21:39,pay_in,USD,5000.00,Peer Oke Richelsen (peer@cal.com),Alan Castro,https://app.deel.com/contract/n2x3nqnej9mj,https://app.deel.com/invoice/6175327,,,,
|
||||||
|
27496726,2022-12-14 03:56:41,pay_out,USD,-1200.00,,,,,,bank_transfer,Alan Esteban Castro Ayala,
|
||||||
|
27408605,2022-12-09 08:45:45,pay_out,USD,-1000.00,,,,,,bank_transfer,Alan Esteban Castro Ayala,
|
||||||
|
27167018,2022-12-01 08:04:25,pay_out,USD,-250.00,,,,,,bank_transfer,Mariana Stone Zamudio,
|
||||||
|
26884837,2022-11-27 08:11:22,pay_out,USD,-1000.00,,,,,,bank_transfer,Alan Esteban Castro Ayala,
|
||||||
|
26837842,2022-11-25 02:52:37,pay_in,USD,6667.00,Peer Oke Richelsen (peer@cal.com),Alan Castro,https://app.deel.com/contract/n2x3nqnej9mj,https://app.deel.com/invoice/5318590,,,,
|
||||||
|
26727923,2022-11-17 05:57:14,pay_out,USD,-500.00,,,,,,bank_transfer,Alan Esteban Castro Ayala,
|
||||||
|
26625501,2022-11-13 06:21:05,pay_out,USD,-700.00,,,,,,bank_transfer,Alan Esteban Castro Ayala,
|
||||||
|
26603039,2022-11-11 02:56:29,pay_out,USD,-1100.00,,,,,,bank_transfer,Alan Esteban Castro Ayala,
|
||||||
|
26157094,2022-10-28 01:55:27,pay_out,USD,-520.00,,,,,,bank_transfer,Mariana Stone Zamudio,
|
||||||
|
26077365,2022-10-26 06:00:50,pay_in,USD,6667.00,Peer Oke Richelsen (peer@cal.com),Alan Castro,https://app.deel.com/contract/n2x3nqnej9mj,https://app.deel.com/invoice/4903275,,,,
|
||||||
|
25975601,2022-10-21 03:06:21,pay_out,USD,-700.00,,,,,,bank_transfer,Alan Esteban Castro Ayala,
|
||||||
|
25941834,2022-10-19 09:23:39,pay_out,USD,-360.00,,,,,,bank_transfer,Mariana Stone Zamudio,
|
||||||
|
25742630,2022-10-09 11:25:50,pay_out,USD,-600.00,,,,,,bank_transfer,Alan Esteban Castro Ayala,
|
||||||
|
25529030,2022-10-01 03:41:43,pay_out,USD,-700.00,,,,,,bank_transfer,Alan Esteban Castro Ayala,
|
||||||
|
25473460,2022-09-30 03:56:23,pay_out,USD,-500.00,,,,,,bank_transfer,Mariana Stone Zamudio,
|
||||||
|
25339929,2022-09-28 04:16:37,pay_out,USD,-1000.00,,,,,,bank_transfer,Alan Esteban Castro Ayala,
|
||||||
|
25318976,2022-09-27 09:57:23,pay_in,USD,6667.00,Peer Oke Richelsen (peer@cal.com),Alan Castro,https://app.deel.com/contract/n2x3nqnej9mj,https://app.deel.com/invoice/4458047,,,,
|
||||||
|
25238452,2022-09-24 05:08:01,pay_out,USD,-500.00,,,,,,bank_transfer,Mariana Stone Zamudio,
|
||||||
|
25238450,2022-09-24 05:06:59,pay_out,USD,-1000.00,,,,,,bank_transfer,Alan Esteban Castro Ayala,
|
||||||
|
25225884,2022-09-23 02:35:35,pay_out,USD,-700.00,,,,,,bank_transfer,Alan Esteban Castro Ayala,
|
||||||
|
25015780,2022-09-10 03:26:39,pay_out,USD,-1000.00,,,,,,bank_transfer,Alan Esteban Castro Ayala,
|
||||||
|
24972244,2022-09-08 03:06:08,pay_out,USD,-1200.00,,,,,,bank_transfer,Alan Esteban Castro Ayala,
|
||||||
|
24638989,2022-08-30 08:19:07,pay_out,USD,-1000.00,,,,,,bank_transfer,Alan Esteban Castro Ayala,
|
||||||
|
24561565,2022-08-26 04:18:47,pay_out,USD,-2500.00,,,,,,bank_transfer,Alan Esteban Castro Ayala,
|
||||||
|
24512685,2022-08-25 06:00:46,pay_in,USD,6667.00,Peer Oke Richelsen (peer@cal.com),Alan Castro,https://app.deel.com/contract/n2x3nqnej9mj,https://app.deel.com/invoice/4028947,,,,
|
||||||
|
24462509,2022-08-22 12:53:39,pay_out,USD,-1000.00,,,,,,bank_transfer,Alan Esteban Castro Ayala,
|
||||||
|
24400743,2022-08-16 07:25:30,pay_out,USD,-683.05,,,,,,,,
|
||||||
|
24204613,2022-08-05 02:47:49,pay_out,USD,-605.00,,,,,,,,
|
||||||
|
24145927,2022-08-03 04:09:21,pay_out,USD,-700.00,,,,,,bank_transfer,Alan Esteban Castro Ayala,
|
||||||
|
24126057,2022-08-02 04:13:21,pay_out,USD,-800.00,,,,,,bank_transfer,Alan Esteban Castro Ayala,
|
||||||
|
24017491,2022-07-29 04:56:07,pay_out,USD,-1360.00,,,,,,,,
|
||||||
|
23847943,2022-07-26 08:39:45,pay_out,USD,-1010.00,,,,,,bank_transfer,Alan Esteban Castro Ayala,
|
||||||
|
23846468,2022-07-26 06:00:48,pay_in,USD,6667.00,Peer Oke Richelsen (peer@cal.com),Alan Castro,https://app.deel.com/contract/n2x3nqnej9mj,https://app.deel.com/invoice/3724054,,,,
|
||||||
|
23742743,2022-07-20 11:31:40,pay_out,USD,-700.00,,,,,,bank_transfer,Alan Esteban Castro Ayala,
|
||||||
|
23621871,2022-07-13 08:24:38,pay_out,USD,-500.00,,,,,,bank_transfer,Alan Esteban Castro Ayala,
|
||||||
|
23376935,2022-07-01 07:55:11,pay_out,USD,-1050.00,,,,,,bank_transfer,Alan Esteban Castro Ayala,
|
||||||
|
23171285,2022-06-28 03:00:24,pay_in,USD,6667.00,Peer Oke Richelsen (peer@cal.com),Alan Castro,https://app.deel.com/contract/n2x3nqnej9mj,https://app.deel.com/invoice/3367214,,,,
|
||||||
|
22926590,2022-06-13 04:44:26,pay_out,USD,-1000.00,,,,,,bank_transfer,Alan Esteban Castro Ayala,
|
||||||
|
22694913,2022-06-01 04:47:15,pay_out,USD,-450.00,,,,,,,,
|
||||||
|
22694893,2022-06-01 04:43:14,pay_out,USD,-1500.00,,,,,,bank_transfer,Alan Esteban Castro Ayala,
|
||||||
|
22519886,2022-05-28 04:00:12,pay_in,USD,6667.00,Peer Oke Richelsen (peer@cal.com),Alan Castro,https://app.deel.com/contract/n2x3nqnej9mj,https://app.deel.com/invoice/3031667,,,,
|
||||||
|
22391139,2022-05-21 02:13:17,pay_out,USD,-500.00,,,,,,bank_transfer,Alan Esteban Castro Ayala,
|
||||||
|
22259676,2022-05-11 06:56:04,pay_out,USD,-1200.00,,,,,,bank_transfer,Alan Esteban Castro Ayala,
|
||||||
|
21962793,2022-04-28 06:08:11,pay_out,USD,-500.00,,,,,,,,
|
||||||
|
21922875,2022-04-27 06:13:38,pay_out,USD,-500.00,,,,,,,,
|
||||||
|
21922469,2022-04-27 05:47:33,pay_out,USD,-500.00,,,,,,,,
|
||||||
|
21922168,2022-04-27 05:30:25,pay_in,USD,11667.00,Peer Oke Richelsen (peer@cal.com),Alan Castro,https://app.deel.com/contract/n2x3nqnej9mj,https://app.deel.com/invoice/2982328,,,,
|
||||||
|
21862819,2022-04-26 09:33:22,pay_out,USD,-1650.00,,,,,,bank_transfer,Alan Esteban Castro Ayala,
|
||||||
|
21319274,2022-03-28 06:30:29,pay_in,USD,2000.00,Peer Oke Richelsen (peer@cal.com),Alan Castro,https://app.deel.com/contract/avqlrOd7,https://app.deel.com/invoice/2479494,,,,
|
|
|
@ -0,0 +1,12 @@
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { eventTypeAppCardZod } from "../eventTypeAppCardZod";
|
||||||
|
|
||||||
|
export const appDataSchema = eventTypeAppCardZod.merge(
|
||||||
|
z.object({
|
||||||
|
price: z.number(),
|
||||||
|
currency: z.string(),
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
export const appKeysSchema = z.object({});
|
|
@ -22,6 +22,7 @@
|
||||||
"@calcom/trpc": "*",
|
"@calcom/trpc": "*",
|
||||||
"@calcom/ui": "*",
|
"@calcom/ui": "*",
|
||||||
"@calcom/zoomvideo": "*",
|
"@calcom/zoomvideo": "*",
|
||||||
|
"@types/mercadopago": "^1.5.8",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"qs-stringify": "^1.2.1"
|
"qs-stringify": "^1.2.1"
|
||||||
},
|
},
|
||||||
|
|
|
@ -78,7 +78,7 @@ export const AvailableTimes = ({
|
||||||
data-testid="time"
|
data-testid="time"
|
||||||
data-time={slot.time}
|
data-time={slot.time}
|
||||||
onClick={() => onTimeSelect(slot.time)}
|
onClick={() => onTimeSelect(slot.time)}
|
||||||
className="mb-2 flex h-auto min-h-9 w-full flex-col justify-center py-2"
|
className="min-h-9 mb-2 flex h-auto w-full flex-col justify-center py-2"
|
||||||
color="secondary">
|
color="secondary">
|
||||||
{dayjs.utc(slot.time).tz(timezone).format(timeFormat)}
|
{dayjs.utc(slot.time).tz(timezone).format(timeFormat)}
|
||||||
{bookingFull && <p className="text-sm">{t("booking_full")}</p>}
|
{bookingFull && <p className="text-sm">{t("booking_full")}</p>}
|
||||||
|
|
|
@ -4,6 +4,7 @@ import type { FC } from "react";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
import { getSuccessPageLocationMessage } from "@calcom/app-store/locations";
|
import { getSuccessPageLocationMessage } from "@calcom/app-store/locations";
|
||||||
|
import { MercadoPagoPaymentComponent } from "@calcom/app-store/mercado_pago/components/MercadoPagoPaymentComponent";
|
||||||
import dayjs from "@calcom/dayjs";
|
import dayjs from "@calcom/dayjs";
|
||||||
import { sdkActionManager, useIsEmbed } from "@calcom/embed-core/embed-iframe";
|
import { sdkActionManager, useIsEmbed } from "@calcom/embed-core/embed-iframe";
|
||||||
import { APP_NAME, WEBSITE_URL } from "@calcom/lib/constants";
|
import { APP_NAME, WEBSITE_URL } from "@calcom/lib/constants";
|
||||||
|
@ -128,6 +129,16 @@ const PaymentPage: FC<PaymentPageProps> = (props) => {
|
||||||
bookingUid={props.booking.uid}
|
bookingUid={props.booking.uid}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
{props.payment.appId === "mercado_pago" && !props.payment.success && (
|
||||||
|
<MercadoPagoPaymentComponent
|
||||||
|
payment={props.payment}
|
||||||
|
eventType={props.eventType}
|
||||||
|
user={props.user}
|
||||||
|
location={props.booking.location}
|
||||||
|
bookingId={props.booking.id}
|
||||||
|
bookingUid={props.booking.uid}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
{props.payment.refunded && (
|
{props.payment.refunded && (
|
||||||
<div className="text-default mt-4 text-center dark:text-gray-300">{t("refunded")}</div>
|
<div className="text-default mt-4 text-center dark:text-gray-300">{t("refunded")}</div>
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -299,10 +299,10 @@ function UserDropdown({ small }: { small?: boolean }) {
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
{!user.away && (
|
{!user.away && (
|
||||||
<div className="absolute bottom-0 right-0 h-3 w-3 rounded-full border-2 border-muted bg-green-500" />
|
<div className="border-muted absolute bottom-0 right-0 h-3 w-3 rounded-full border-2 bg-green-500" />
|
||||||
)}
|
)}
|
||||||
{user.away && (
|
{user.away && (
|
||||||
<div className="absolute bottom-0 right-0 h-3 w-3 rounded-full border-2 border-muted bg-yellow-500" />
|
<div className="border-muted absolute bottom-0 right-0 h-3 w-3 rounded-full border-2 bg-yellow-500" />
|
||||||
)}
|
)}
|
||||||
</span>
|
</span>
|
||||||
{!small && (
|
{!small && (
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import type { AppCategories, Prisma } from "@prisma/client";
|
import type { AppCategories, Prisma, PaymentOption } from "@prisma/client";
|
||||||
|
|
||||||
import appStore from "@calcom/app-store";
|
import appStore from "@calcom/app-store";
|
||||||
import type { EventTypeAppsList } from "@calcom/app-store/utils";
|
import type { EventTypeAppsList } from "@calcom/app-store/utils";
|
||||||
|
@ -25,6 +25,7 @@ const handlePayment = async (
|
||||||
bookerEmail: string
|
bookerEmail: string
|
||||||
) => {
|
) => {
|
||||||
const paymentApp = await appStore[paymentAppCredentials?.app?.dirName as keyof typeof appStore];
|
const paymentApp = await appStore[paymentAppCredentials?.app?.dirName as keyof typeof appStore];
|
||||||
|
|
||||||
if (!(paymentApp && "lib" in paymentApp && "PaymentService" in paymentApp.lib)) {
|
if (!(paymentApp && "lib" in paymentApp && "PaymentService" in paymentApp.lib)) {
|
||||||
console.warn(`payment App service of type ${paymentApp} is not implemented`);
|
console.warn(`payment App service of type ${paymentApp} is not implemented`);
|
||||||
return null;
|
return null;
|
||||||
|
@ -32,7 +33,7 @@ const handlePayment = async (
|
||||||
const PaymentService = paymentApp.lib.PaymentService;
|
const PaymentService = paymentApp.lib.PaymentService;
|
||||||
const paymentInstance = new PaymentService(paymentAppCredentials);
|
const paymentInstance = new PaymentService(paymentAppCredentials);
|
||||||
|
|
||||||
const paymentOption =
|
const paymentOption: PaymentOption =
|
||||||
selectedEventType?.metadata?.apps?.[paymentAppCredentials.appId].paymentOption || "ON_BOOKING";
|
selectedEventType?.metadata?.apps?.[paymentAppCredentials.appId].paymentOption || "ON_BOOKING";
|
||||||
|
|
||||||
let paymentData;
|
let paymentData;
|
||||||
|
|
|
@ -0,0 +1,2 @@
|
||||||
|
-- AlterEnum
|
||||||
|
ALTER TYPE "PaymentType" ADD VALUE 'MERCADO_PAGO';
|
|
@ -239,5 +239,11 @@
|
||||||
"slug": "sylapsvideo",
|
"slug": "sylapsvideo",
|
||||||
"type": "sylaps_video",
|
"type": "sylaps_video",
|
||||||
"isTemplate": false
|
"isTemplate": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"dirName": "mercado_pago",
|
||||||
|
"categories": ["payment"],
|
||||||
|
"slug": "mercado_pago",
|
||||||
|
"type": "mercado_pago_payment"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
Loading…
Reference in New Issue
Block a user