Compare commits

...

7 Commits

Author SHA1 Message Date
Alan 8860fa0cae Merge with main 2023-05-02 12:17:43 +01:00
Alan 9ebc65e8c4 Fix style for setup page 2023-04-26 17:06:19 -06:00
Alan 8580c29a92 Added payment API url to redirect to booking page after payment 2023-04-24 17:26:24 -07:00
Alan 94498ea7af merge with main 2023-04-21 13:18:56 -07:00
Alan d331d8c5e4 WIP mercado pago 2023-04-17 11:37:06 -07:00
Alan f26368f405 merge with main 2023-04-14 14:43:58 -07:00
Alan 7be836d834 merge with main 2023-02-20 13:20:06 -07:00
45 changed files with 1711 additions and 1296 deletions

View File

@ -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=""

1
apps/api Submodule

@ -0,0 +1 @@
Subproject commit 604d937661ed8f8fd50cc645bf7d129b635333e8

1
apps/console Submodule

@ -0,0 +1 @@
Subproject commit 8e116959652a773fa7cde5e9f8bf502c054b151b

View File

@ -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",

View File

@ -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"
}

1
apps/website Submodule

@ -0,0 +1 @@
Subproject commit a4b58535c2716815b18cd88e7d478570012b739d

View File

@ -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 }) => {

View File

@ -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")),

View File

@ -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,

View File

@ -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,

View File

@ -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,

View File

@ -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"),

View File

@ -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"),

View File

@ -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
---
.

View File

@ -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;

View File

@ -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()),
});

View File

@ -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" });
}

View File

@ -0,0 +1,3 @@
export { default as add } from "./add";
export { default as payment } from "./payment";
export { default as webhook } from "./_webhook";

View File

@ -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();

View File

@ -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;

View File

@ -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>
);
};

View File

@ -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"
}

View File

@ -0,0 +1,3 @@
export * as api from "./api";
export * as lib from "./lib";
export { metadata } from "./_metadata";

View File

@ -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.");
}
}

View File

@ -0,0 +1 @@
export * from "./PaymentService";

View File

@ -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;
}

View File

@ -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": "*"
}
}

View File

@ -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,
},
};
};

View File

@ -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&apos;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

View File

@ -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,,,,
1 ID Date Transaction Type Currency Amount Client Contract Name Contract URL Invoice URL Invoice Description Withdraw Method Withdraw Account Holder Name Withdraw Account Number
2 28407799 2023-01-17 01:52:38 pay_out USD -1200.00 bank_transfer Alan Esteban Castro Ayala
3 28048891 2023-01-02 09:16:39 pay_out USD -600.00 bank_transfer Alan Esteban Castro Ayala
4 28006521 2022-12-31 12:16:23 pay_out USD -800.00 bank_transfer Alan Esteban Castro Ayala
5 27895486 2022-12-28 01:10:11 pay_out USD -1000.00 bank_transfer Alan Esteban Castro Ayala
6 27762420 2022-12-22 06:05:58 pay_out USD -1250.00 bank_transfer Alan Esteban Castro Ayala
7 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
8 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
9 27496726 2022-12-14 03:56:41 pay_out USD -1200.00 bank_transfer Alan Esteban Castro Ayala
10 27408605 2022-12-09 08:45:45 pay_out USD -1000.00 bank_transfer Alan Esteban Castro Ayala
11 27167018 2022-12-01 08:04:25 pay_out USD -250.00 bank_transfer Mariana Stone Zamudio
12 26884837 2022-11-27 08:11:22 pay_out USD -1000.00 bank_transfer Alan Esteban Castro Ayala
13 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
14 26727923 2022-11-17 05:57:14 pay_out USD -500.00 bank_transfer Alan Esteban Castro Ayala
15 26625501 2022-11-13 06:21:05 pay_out USD -700.00 bank_transfer Alan Esteban Castro Ayala
16 26603039 2022-11-11 02:56:29 pay_out USD -1100.00 bank_transfer Alan Esteban Castro Ayala
17 26157094 2022-10-28 01:55:27 pay_out USD -520.00 bank_transfer Mariana Stone Zamudio
18 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
19 25975601 2022-10-21 03:06:21 pay_out USD -700.00 bank_transfer Alan Esteban Castro Ayala
20 25941834 2022-10-19 09:23:39 pay_out USD -360.00 bank_transfer Mariana Stone Zamudio
21 25742630 2022-10-09 11:25:50 pay_out USD -600.00 bank_transfer Alan Esteban Castro Ayala
22 25529030 2022-10-01 03:41:43 pay_out USD -700.00 bank_transfer Alan Esteban Castro Ayala
23 25473460 2022-09-30 03:56:23 pay_out USD -500.00 bank_transfer Mariana Stone Zamudio
24 25339929 2022-09-28 04:16:37 pay_out USD -1000.00 bank_transfer Alan Esteban Castro Ayala
25 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
26 25238452 2022-09-24 05:08:01 pay_out USD -500.00 bank_transfer Mariana Stone Zamudio
27 25238450 2022-09-24 05:06:59 pay_out USD -1000.00 bank_transfer Alan Esteban Castro Ayala
28 25225884 2022-09-23 02:35:35 pay_out USD -700.00 bank_transfer Alan Esteban Castro Ayala
29 25015780 2022-09-10 03:26:39 pay_out USD -1000.00 bank_transfer Alan Esteban Castro Ayala
30 24972244 2022-09-08 03:06:08 pay_out USD -1200.00 bank_transfer Alan Esteban Castro Ayala
31 24638989 2022-08-30 08:19:07 pay_out USD -1000.00 bank_transfer Alan Esteban Castro Ayala
32 24561565 2022-08-26 04:18:47 pay_out USD -2500.00 bank_transfer Alan Esteban Castro Ayala
33 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
34 24462509 2022-08-22 12:53:39 pay_out USD -1000.00 bank_transfer Alan Esteban Castro Ayala
35 24400743 2022-08-16 07:25:30 pay_out USD -683.05
36 24204613 2022-08-05 02:47:49 pay_out USD -605.00
37 24145927 2022-08-03 04:09:21 pay_out USD -700.00 bank_transfer Alan Esteban Castro Ayala
38 24126057 2022-08-02 04:13:21 pay_out USD -800.00 bank_transfer Alan Esteban Castro Ayala
39 24017491 2022-07-29 04:56:07 pay_out USD -1360.00
40 23847943 2022-07-26 08:39:45 pay_out USD -1010.00 bank_transfer Alan Esteban Castro Ayala
41 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
42 23742743 2022-07-20 11:31:40 pay_out USD -700.00 bank_transfer Alan Esteban Castro Ayala
43 23621871 2022-07-13 08:24:38 pay_out USD -500.00 bank_transfer Alan Esteban Castro Ayala
44 23376935 2022-07-01 07:55:11 pay_out USD -1050.00 bank_transfer Alan Esteban Castro Ayala
45 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
46 22926590 2022-06-13 04:44:26 pay_out USD -1000.00 bank_transfer Alan Esteban Castro Ayala
47 22694913 2022-06-01 04:47:15 pay_out USD -450.00
48 22694893 2022-06-01 04:43:14 pay_out USD -1500.00 bank_transfer Alan Esteban Castro Ayala
49 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
50 22391139 2022-05-21 02:13:17 pay_out USD -500.00 bank_transfer Alan Esteban Castro Ayala
51 22259676 2022-05-11 06:56:04 pay_out USD -1200.00 bank_transfer Alan Esteban Castro Ayala
52 21962793 2022-04-28 06:08:11 pay_out USD -500.00
53 21922875 2022-04-27 06:13:38 pay_out USD -500.00
54 21922469 2022-04-27 05:47:33 pay_out USD -500.00
55 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
56 21862819 2022-04-26 09:33:22 pay_out USD -1650.00 bank_transfer Alan Esteban Castro Ayala
57 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

View File

@ -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({});

View File

@ -22,6 +22,7 @@
"@calcom/trpc": "*",
"@calcom/ui": "*",
"@calcom/zoomvideo": "*",
"@types/mercadopago": "^1.5.8",
"lodash": "^4.17.21",
"qs-stringify": "^1.2.1"
},

View File

@ -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>}

View File

@ -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>
)}

View File

@ -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 && (

View File

@ -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;

View File

@ -0,0 +1,2 @@
-- AlterEnum
ALTER TYPE "PaymentType" ADD VALUE 'MERCADO_PAGO';

View File

@ -239,5 +239,11 @@
"slug": "sylapsvideo",
"type": "sylaps_video",
"isTemplate": false
},
{
"dirName": "mercado_pago",
"categories": ["payment"],
"slug": "mercado_pago",
"type": "mercado_pago_payment"
}
]

1522
yarn.lock

File diff suppressed because it is too large Load Diff