* refactor: remove circular dependency between prisma and app-store packages - Replace EventTypeAppMetadataSchema with z.record(z.any()).optional() pattern - Remove appDataSchemas import from packages/prisma/zod-utils.ts - Add null checks in consuming packages for flexible validation - Fix test file that no longer needs @ts-expect-error directive This breaks the circular dependency while maintaining all functionality by moving strict validation to the business logic layer where operations actually happen, following existing patterns in the codebase. Co-Authored-By: keith@cal.com <keithwillcode@gmail.com> * refactor: remove EventTypeAppMetadataSchema exports from prisma package - Remove EventTypeAppMetadataSchema and eventTypeAppMetadataOptionalSchema exports from prisma/zod-utils.ts - Update all importing files to use local z.record(z.any()).optional() schemas - Replace type annotations with Record<string, any> where appropriate - Maintain validation functionality while breaking circular dependency - All TypeScript compilation now passes without errors Co-Authored-By: keith@cal.com <keithwillcode@gmail.com> * refactor: complete removal of EventTypeAppMetadataSchema from remaining files - Update handleSeats/createNewSeat.ts to use local schema - Update payment handlers to use local schemas - Update eventTypes update handler to use local schema - All files now define their own validation instead of importing from prisma - Circular dependency completely eliminated Co-Authored-By: keith@cal.com <keithwillcode@gmail.com> * fix: resolve TypeScript compilation errors - Remove duplicate z import from handleConfirmation.ts - Remove duplicate appDataSchemas import from update.handler.ts - Fix index signature errors in handlePayment.ts by using appData variable consistently - All type checks now pass successfully Co-Authored-By: keith@cal.com <keithwillcode@gmail.com> * fix: add type casting for appSlug in eventTypeService - Cast appSlug as keyof typeof apps to resolve index signature error - Maintains type safety while allowing dynamic property access Co-Authored-By: keith@cal.com <keithwillcode@gmail.com> * fix * remove * make zod-utils.ts in app-store * update imports * fix * fix * fix * revert unrelated change * update imports * fix * fix * revert * fix * fix --------- Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Co-authored-by: hbjORbj <sldisek783@gmail.com>
195 lines
6.9 KiB
TypeScript
195 lines
6.9 KiB
TypeScript
import { buffer } from "micro";
|
|
import type { NextApiRequest, NextApiResponse } from "next";
|
|
import type Stripe from "stripe";
|
|
|
|
import { handlePaymentSuccess } from "@calcom/app-store/_utils/payments/handlePaymentSuccess";
|
|
import { eventTypeMetaDataSchemaWithTypedApps } from "@calcom/app-store/zod-utils";
|
|
import { sendAttendeeRequestEmailAndSMS, sendOrganizerRequestEmail } from "@calcom/emails";
|
|
import { doesBookingRequireConfirmation } from "@calcom/features/bookings/lib/doesBookingRequireConfirmation";
|
|
import { getAllCredentialsIncludeServiceAccountKey } from "@calcom/features/bookings/lib/getAllCredentialsForUsersOnEvent/getAllCredentials";
|
|
import { handleConfirmation } from "@calcom/features/bookings/lib/handleConfirmation";
|
|
import stripe from "@calcom/features/ee/payments/server/stripe";
|
|
import { getPlatformParams } from "@calcom/features/platform-oauth-client/get-platform-params";
|
|
import { PlatformOAuthClientRepository } from "@calcom/features/platform-oauth-client/platform-oauth-client.repository";
|
|
import EventManager, { placeholderCreatedEvent } from "@calcom/lib/EventManager";
|
|
import { IS_PRODUCTION } from "@calcom/lib/constants";
|
|
import { getErrorFromUnknown } from "@calcom/lib/errors";
|
|
import { HttpError as HttpCode } from "@calcom/lib/http-error";
|
|
import logger from "@calcom/lib/logger";
|
|
import { getBooking } from "@calcom/lib/payment/getBooking";
|
|
import { safeStringify } from "@calcom/lib/safeStringify";
|
|
import { prisma } from "@calcom/prisma";
|
|
import type { Prisma } from "@calcom/prisma/client";
|
|
import { BookingStatus } from "@calcom/prisma/enums";
|
|
|
|
const log = logger.getSubLogger({ prefix: ["[paymentWebhook]"] });
|
|
|
|
export const config = {
|
|
api: {
|
|
bodyParser: false,
|
|
},
|
|
};
|
|
|
|
export async function handleStripePaymentSuccess(event: Stripe.Event) {
|
|
const paymentIntent = event.data.object as Stripe.PaymentIntent;
|
|
const payment = await prisma.payment.findFirst({
|
|
where: {
|
|
externalId: paymentIntent.id,
|
|
},
|
|
select: {
|
|
id: true,
|
|
bookingId: true,
|
|
},
|
|
});
|
|
|
|
if (!payment?.bookingId) {
|
|
log.error("Stripe: Payment Not Found", safeStringify(paymentIntent), safeStringify(payment));
|
|
throw new HttpCode({ statusCode: 204, message: "Payment not found" });
|
|
}
|
|
if (!payment?.bookingId) throw new HttpCode({ statusCode: 204, message: "Payment not found" });
|
|
|
|
await handlePaymentSuccess(payment.id, payment.bookingId);
|
|
}
|
|
|
|
const handleSetupSuccess = async (event: Stripe.Event) => {
|
|
const setupIntent = event.data.object as Stripe.SetupIntent;
|
|
const payment = await prisma.payment.findFirst({
|
|
where: {
|
|
externalId: setupIntent.id,
|
|
},
|
|
});
|
|
|
|
if (!payment?.data || !payment?.id) throw new HttpCode({ statusCode: 204, message: "Payment not found" });
|
|
|
|
const { booking, user, evt, eventType } = await getBooking(payment.bookingId);
|
|
|
|
const bookingData: Prisma.BookingUpdateInput = {
|
|
paid: true,
|
|
};
|
|
|
|
if (!user) throw new HttpCode({ statusCode: 204, message: "No user found" });
|
|
|
|
const requiresConfirmation = doesBookingRequireConfirmation({
|
|
booking: {
|
|
...booking,
|
|
eventType,
|
|
},
|
|
});
|
|
|
|
const metadata = eventTypeMetaDataSchemaWithTypedApps.parse(eventType?.metadata);
|
|
const allCredentials = await getAllCredentialsIncludeServiceAccountKey(user, {
|
|
...booking.eventType,
|
|
metadata,
|
|
});
|
|
|
|
const platformOAuthClientRepository = new PlatformOAuthClientRepository();
|
|
const platformOAuthClient = user.isPlatformManaged
|
|
? await platformOAuthClientRepository.getByUserId(user.id)
|
|
: null;
|
|
const areCalendarEventsEnabled = platformOAuthClient?.areCalendarEventsEnabled ?? true;
|
|
const areEmailsEnabled = platformOAuthClient?.areEmailsEnabled ?? true;
|
|
|
|
if (!requiresConfirmation) {
|
|
const eventManager = new EventManager({ ...user, credentials: allCredentials }, metadata?.apps);
|
|
const scheduleResult = areCalendarEventsEnabled
|
|
? await eventManager.create(evt)
|
|
: placeholderCreatedEvent;
|
|
bookingData.references = { create: scheduleResult.referencesToCreate };
|
|
bookingData.status = BookingStatus.ACCEPTED;
|
|
}
|
|
|
|
await prisma.payment.update({
|
|
where: {
|
|
id: payment.id,
|
|
},
|
|
data: {
|
|
data: {
|
|
...(payment.data as Prisma.JsonObject),
|
|
setupIntent: setupIntent as unknown as Prisma.JsonObject,
|
|
},
|
|
booking: {
|
|
update: {
|
|
...bookingData,
|
|
},
|
|
},
|
|
},
|
|
});
|
|
|
|
// If the card information was already captured in the same customer. Delete the previous payment method
|
|
|
|
if (!requiresConfirmation) {
|
|
await handleConfirmation({
|
|
user: { ...user, credentials: allCredentials },
|
|
evt,
|
|
prisma,
|
|
bookingId: booking.id,
|
|
booking,
|
|
paid: true,
|
|
platformClientParams: platformOAuthClient ? getPlatformParams(platformOAuthClient) : undefined,
|
|
});
|
|
} else if (areEmailsEnabled) {
|
|
await sendOrganizerRequestEmail({ ...evt }, eventType.metadata);
|
|
await sendAttendeeRequestEmailAndSMS({ ...evt }, evt.attendees[0], eventType.metadata);
|
|
}
|
|
};
|
|
|
|
type WebhookHandler = (event: Stripe.Event) => Promise<void>;
|
|
|
|
const webhookHandlers: Record<string, WebhookHandler | undefined> = {
|
|
"payment_intent.succeeded": handleStripePaymentSuccess,
|
|
"setup_intent.succeeded": handleSetupSuccess,
|
|
};
|
|
|
|
/**
|
|
* @deprecated
|
|
* We need to create a PaymentManager in `@calcom/lib`
|
|
* to prevent circular dependencies on App Store migration
|
|
*/
|
|
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
|
try {
|
|
if (req.method !== "POST") {
|
|
throw new HttpCode({ statusCode: 405, message: "Method Not Allowed" });
|
|
}
|
|
const sig = req.headers["stripe-signature"];
|
|
if (!sig) {
|
|
throw new HttpCode({ statusCode: 400, message: "Missing stripe-signature" });
|
|
}
|
|
|
|
if (!process.env.STRIPE_WEBHOOK_SECRET) {
|
|
throw new HttpCode({ statusCode: 500, message: "Missing process.env.STRIPE_WEBHOOK_SECRET" });
|
|
}
|
|
const requestBuffer = await buffer(req);
|
|
const payload = requestBuffer.toString();
|
|
|
|
const event = stripe.webhooks.constructEvent(payload, sig, process.env.STRIPE_WEBHOOK_SECRET);
|
|
|
|
// bypassing this validation for e2e tests
|
|
// in order to successfully confirm the payment
|
|
if (!event.account && !process.env.NEXT_PUBLIC_IS_E2E) {
|
|
throw new HttpCode({ statusCode: 202, message: "Incoming connected account" });
|
|
}
|
|
|
|
const handler = webhookHandlers[event.type];
|
|
if (handler) {
|
|
await handler(event);
|
|
} else {
|
|
/** Not really an error, just letting Stripe know that the webhook was received but unhandled */
|
|
throw new HttpCode({
|
|
statusCode: 202,
|
|
message: `Unhandled Stripe Webhook event type ${event.type}`,
|
|
});
|
|
}
|
|
} catch (_err) {
|
|
const err = getErrorFromUnknown(_err);
|
|
console.error(`Webhook Error: ${err.message}`);
|
|
res.status(err.statusCode ?? 500).send({
|
|
message: err.message,
|
|
stack: IS_PRODUCTION ? undefined : err.stack,
|
|
});
|
|
return;
|
|
}
|
|
|
|
// Return a response to acknowledge receipt of the event
|
|
res.json({ received: true });
|
|
}
|