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=""
|
||||
|
||||
# *********************************************************************************************************
|
||||
|
||||
|
||||
# - 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",
|
||||
"md5": "^2.3.0",
|
||||
"memory-cache": "^0.2.0",
|
||||
"mercadopago": "^1.5.14",
|
||||
"micro": "^10.0.1",
|
||||
"mime-types": "^2.1.35",
|
||||
"next": "^13.2.1",
|
||||
|
|
|
@ -1799,6 +1799,7 @@
|
|||
"charge_attendee": "Charge attendee {{amount, currency}}",
|
||||
"payment_app_commission": "Require payment ({{paymentFeePercentage}}% + {{fee, currency}} commission per transaction)",
|
||||
"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")),
|
||||
closecom: dynamic(() => import("../../closecom/pages/setup")),
|
||||
sendgrid: dynamic(() => import("../../sendgrid/pages/setup")),
|
||||
mercado_pago: dynamic(() => import("../../mercado_pago/pages/setup")),
|
||||
};
|
||||
|
||||
export const AppSetupPage = (props: { slug: string }) => {
|
||||
|
|
|
@ -24,6 +24,7 @@ export const EventTypeAddonMap = {
|
|||
ga4: dynamic(() => import("./ga4/components/EventTypeAppCardInterface")),
|
||||
giphy: dynamic(() => import("./giphy/components/EventTypeAppCardInterface")),
|
||||
gtm: dynamic(() => import("./gtm/components/EventTypeAppCardInterface")),
|
||||
mercado_pago: dynamic(() => import("./mercado_pago/components/EventTypeAppCardInterface")),
|
||||
metapixel: dynamic(() => import("./metapixel/components/EventTypeAppCardInterface")),
|
||||
plausible: dynamic(() => import("./plausible/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 hubspot_zod_ts } from "./hubspot/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 office365calendar_zod_ts } from "./office365calendar/zod";
|
||||
import { appKeysSchema as office365video_zod_ts } from "./office365video/zod";
|
||||
|
@ -37,6 +38,7 @@ export const appKeysSchemas = {
|
|||
gtm: gtm_zod_ts,
|
||||
hubspot: hubspot_zod_ts,
|
||||
larkcalendar: larkcalendar_zod_ts,
|
||||
mercado_pago: mercado_pago_zod_ts,
|
||||
metapixel: metapixel_zod_ts,
|
||||
office365calendar: office365calendar_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 jitsivideo__metadata_ts } from "./jitsivideo/_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 n8n_config_json from "./n8n/config.json";
|
||||
import { metadata as office365calendar__metadata_ts } from "./office365calendar/_metadata";
|
||||
|
@ -87,6 +88,7 @@ export const appStoreMetadata = {
|
|||
huddle01video: huddle01video__metadata_ts,
|
||||
jitsivideo: jitsivideo__metadata_ts,
|
||||
larkcalendar: larkcalendar__metadata_ts,
|
||||
mercado_pago: mercado_pago_config_json,
|
||||
metapixel: metapixel_config_json,
|
||||
n8n: n8n_config_json,
|
||||
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 hubspot_zod_ts } from "./hubspot/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 office365calendar_zod_ts } from "./office365calendar/zod";
|
||||
import { appDataSchema as office365video_zod_ts } from "./office365video/zod";
|
||||
|
@ -37,6 +38,7 @@ export const appDataSchemas = {
|
|||
gtm: gtm_zod_ts,
|
||||
hubspot: hubspot_zod_ts,
|
||||
larkcalendar: larkcalendar_zod_ts,
|
||||
mercado_pago: mercado_pago_zod_ts,
|
||||
metapixel: metapixel_zod_ts,
|
||||
office365calendar: office365calendar_zod_ts,
|
||||
office365video: office365video_zod_ts,
|
||||
|
|
|
@ -25,6 +25,7 @@ export const apiHandlers = {
|
|||
huddle01video: import("./huddle01video/api"),
|
||||
jitsivideo: import("./jitsivideo/api"),
|
||||
larkcalendar: import("./larkcalendar/api"),
|
||||
mercado_pago: import("./mercado_pago/api"),
|
||||
metapixel: import("./metapixel/api"),
|
||||
n8n: import("./n8n/api"),
|
||||
office365calendar: import("./office365calendar/api"),
|
||||
|
|
|
@ -10,6 +10,7 @@ const appStore = {
|
|||
huddle01video: import("./huddle01video"),
|
||||
jitsivideo: import("./jitsivideo"),
|
||||
larkcalendar: import("./larkcalendar"),
|
||||
mercado_pago: import("./mercado_pago"),
|
||||
office365calendar: import("./office365calendar"),
|
||||
office365video: import("./office365video"),
|
||||
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/ui": "*",
|
||||
"@calcom/zoomvideo": "*",
|
||||
"@types/mercadopago": "^1.5.8",
|
||||
"lodash": "^4.17.21",
|
||||
"qs-stringify": "^1.2.1"
|
||||
},
|
||||
|
|
|
@ -78,7 +78,7 @@ export const AvailableTimes = ({
|
|||
data-testid="time"
|
||||
data-time={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">
|
||||
{dayjs.utc(slot.time).tz(timezone).format(timeFormat)}
|
||||
{bookingFull && <p className="text-sm">{t("booking_full")}</p>}
|
||||
|
|
|
@ -4,6 +4,7 @@ import type { FC } from "react";
|
|||
import { useEffect, useState } from "react";
|
||||
|
||||
import { getSuccessPageLocationMessage } from "@calcom/app-store/locations";
|
||||
import { MercadoPagoPaymentComponent } from "@calcom/app-store/mercado_pago/components/MercadoPagoPaymentComponent";
|
||||
import dayjs from "@calcom/dayjs";
|
||||
import { sdkActionManager, useIsEmbed } from "@calcom/embed-core/embed-iframe";
|
||||
import { APP_NAME, WEBSITE_URL } from "@calcom/lib/constants";
|
||||
|
@ -128,6 +129,16 @@ const PaymentPage: FC<PaymentPageProps> = (props) => {
|
|||
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 && (
|
||||
<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 && (
|
||||
<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 && (
|
||||
<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>
|
||||
{!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 type { EventTypeAppsList } from "@calcom/app-store/utils";
|
||||
|
@ -25,6 +25,7 @@ const handlePayment = async (
|
|||
bookerEmail: string
|
||||
) => {
|
||||
const paymentApp = await appStore[paymentAppCredentials?.app?.dirName as keyof typeof appStore];
|
||||
|
||||
if (!(paymentApp && "lib" in paymentApp && "PaymentService" in paymentApp.lib)) {
|
||||
console.warn(`payment App service of type ${paymentApp} is not implemented`);
|
||||
return null;
|
||||
|
@ -32,7 +33,7 @@ const handlePayment = async (
|
|||
const PaymentService = paymentApp.lib.PaymentService;
|
||||
const paymentInstance = new PaymentService(paymentAppCredentials);
|
||||
|
||||
const paymentOption =
|
||||
const paymentOption: PaymentOption =
|
||||
selectedEventType?.metadata?.apps?.[paymentAppCredentials.appId].paymentOption || "ON_BOOKING";
|
||||
|
||||
let paymentData;
|
||||
|
|
|
@ -0,0 +1,2 @@
|
|||
-- AlterEnum
|
||||
ALTER TYPE "PaymentType" ADD VALUE 'MERCADO_PAGO';
|
|
@ -239,5 +239,11 @@
|
|||
"slug": "sylapsvideo",
|
||||
"type": "sylaps_video",
|
||||
"isTemplate": false
|
||||
},
|
||||
{
|
||||
"dirName": "mercado_pago",
|
||||
"categories": ["payment"],
|
||||
"slug": "mercado_pago",
|
||||
"type": "mercado_pago_payment"
|
||||
}
|
||||
]
|
||||
|
|
Loading…
Reference in New Issue
Block a user