-
Notifications
You must be signed in to change notification settings - Fork 12k
feat: Write same metadata to Stripe for booking payment and no show fee payment #23634
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
03eb5bf
ea2a4e2
dc5aa88
c411d2d
b076df4
122fee1
7dd9161
6789b20
ca97b0d
aba2965
6f8dbad
72ea058
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -8,6 +8,7 @@ import { getErrorFromUnknown } from "@calcom/lib/errors"; | |
| import { ErrorWithCode } from "@calcom/lib/errors"; | ||
| import logger from "@calcom/lib/logger"; | ||
| import { safeStringify } from "@calcom/lib/safeStringify"; | ||
| import { BookingRepository } from "@calcom/lib/server/repository/booking"; | ||
| import prisma from "@calcom/prisma"; | ||
| import type { Booking, Payment, PaymentOption, Prisma } from "@calcom/prisma/client"; | ||
| import type { EventTypeMetadata } from "@calcom/prisma/zod-utils"; | ||
|
|
@@ -86,17 +87,16 @@ export class PaymentService implements IAbstractPaymentService { | |
| automatic_payment_methods: { | ||
| enabled: true, | ||
| }, | ||
| metadata: { | ||
| identifier: "cal.com", | ||
| metadata: this.generateMetadata({ | ||
| bookingId, | ||
| calAccountId: userId, | ||
| calUsername: username, | ||
| userId, | ||
| username, | ||
| bookerName, | ||
| bookerEmail: bookerEmail, | ||
| bookerPhoneNumber: bookerPhoneNumber ?? null, | ||
| eventTitle: eventTitle || "", | ||
| bookingTitle: bookingTitle || "", | ||
| }, | ||
| }), | ||
| }; | ||
|
|
||
| const paymentIntent = await this.stripe.paymentIntents.create(params, { | ||
|
|
@@ -217,20 +217,18 @@ export class PaymentService implements IAbstractPaymentService { | |
| } | ||
| } | ||
|
|
||
| async chargeCard(payment: Payment, _bookingId?: Booking["id"]): Promise<Payment> { | ||
| async chargeCard(payment: Payment, bookingId: Booking["id"]): Promise<Payment> { | ||
| try { | ||
| if (!this.credentials) { | ||
| throw new Error("Stripe credentials not found"); | ||
| } | ||
|
|
||
| const stripeAppKeys = await prisma.app.findFirst({ | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This variable is unused |
||
| select: { | ||
| keys: true, | ||
| }, | ||
| where: { | ||
| slug: "stripe", | ||
| }, | ||
| }); | ||
| const bookingRepository = new BookingRepository(prisma); | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We now need to get the booking to generate the metadata to send to Stripe |
||
| const booking = await bookingRepository.findByIdIncludeUserAndAttendees(bookingId); | ||
|
|
||
| if (!booking) { | ||
| throw new Error(`Booking ${bookingId} not found`); | ||
| } | ||
|
|
||
| const paymentObject = payment.data as unknown as StripeSetupIntentData; | ||
|
|
||
|
|
@@ -252,13 +250,27 @@ export class PaymentService implements IAbstractPaymentService { | |
| throw new Error(`Stripe paymentMethod does not exist for setupIntent ${setupIntent.id}`); | ||
| } | ||
|
|
||
| if (!booking.attendees[0]) { | ||
| throw new Error(`Booking attendees are empty for setupIntent ${setupIntent.id}`); | ||
| } | ||
|
|
||
| const params: Stripe.PaymentIntentCreateParams = { | ||
| amount: payment.amount, | ||
| currency: payment.currency, | ||
| customer: setupIntent.customer as string, | ||
| payment_method: setupIntent.payment_method as string, | ||
| off_session: true, | ||
| confirm: true, | ||
| metadata: this.generateMetadata({ | ||
| bookingId, | ||
| userId: booking.user?.id, | ||
| username: booking.user?.username, | ||
| bookerName: booking.attendees[0].name, | ||
| bookerEmail: booking.attendees[0].email, | ||
| bookerPhoneNumber: booking.attendees[0].phoneNumber ?? null, | ||
joeauyeung marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| eventTitle: booking.eventType?.title || null, | ||
| bookingTitle: booking.title, | ||
| }), | ||
|
Comment on lines
+264
to
+273
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion Guard against empty/missing attendees to avoid runtime crash booking.attendees[0] can be undefined (e.g., malformed data, edge imports). Add optional chaining and safe fallbacks. Apply this diff: - metadata: this.generateMetadata({
- bookingId,
- userId: booking.user?.id,
- username: booking.user?.username,
- bookerName: booking.attendees[0].name,
- bookerEmail: booking.attendees[0].email,
- bookerPhoneNumber: booking.attendees[0].phoneNumber ?? null,
- eventTitle: booking.eventType?.title || null,
- bookingTitle: booking.title,
- }),
+ metadata: this.generateMetadata({
+ bookingId,
+ userId: booking.user?.id ?? null,
+ username: booking.user?.username ?? null,
+ bookerName: booking.attendees?.[0]?.name ?? "",
+ bookerEmail: booking.attendees?.[0]?.email ?? "",
+ bookerPhoneNumber: booking.attendees?.[0]?.phoneNumber ?? null,
+ eventTitle: booking.eventType?.title ?? null,
+ bookingTitle: booking.title ?? "",
+ }),🤖 Prompt for AI Agents |
||
| }; | ||
|
|
||
| const paymentIntent = await this.stripe.paymentIntents.create(params, { | ||
|
|
@@ -284,7 +296,7 @@ export class PaymentService implements IAbstractPaymentService { | |
|
|
||
| return paymentData; | ||
| } catch (error) { | ||
| log.error("Stripe: Could not charge card for payment", _bookingId, safeStringify(error)); | ||
| log.error("Stripe: Could not charge card for payment", bookingId, safeStringify(error)); | ||
|
|
||
| const errorMappings = { | ||
| "your card was declined": "your_card_was_declined", | ||
|
|
@@ -422,4 +434,36 @@ export class PaymentService implements IAbstractPaymentService { | |
| isSetupAlready(): boolean { | ||
| return !!this.credentials; | ||
| } | ||
|
|
||
| private generateMetadata({ | ||
| bookingId, | ||
| userId, | ||
| username, | ||
| bookerName, | ||
| bookerEmail, | ||
| bookerPhoneNumber, | ||
| eventTitle, | ||
| bookingTitle, | ||
| }: { | ||
| bookingId: number; | ||
| userId: number | null | undefined; | ||
| username: string | null | undefined; | ||
| bookerName: string; | ||
| bookerEmail: string; | ||
| bookerPhoneNumber: string | null; | ||
| eventTitle: string | null; | ||
| bookingTitle: string; | ||
| }) { | ||
| return { | ||
| identifier: "cal.com", | ||
| bookingId, | ||
| calAccountId: userId ?? null, | ||
| calUsername: username ?? null, | ||
| bookerName, | ||
| bookerEmail: bookerEmail, | ||
| bookerPhoneNumber: bookerPhoneNumber ?? null, | ||
| eventTitle: eventTitle || "", | ||
| bookingTitle: bookingTitle || "", | ||
| }; | ||
| } | ||
hariombalhara marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -387,6 +387,41 @@ export class BookingRepository { | |
| }); | ||
| } | ||
|
|
||
| async findByIdIncludeUserAndAttendees(bookingId: number) { | ||
| return await this.prismaClient.booking.findUnique({ | ||
| where: { | ||
| id: bookingId, | ||
| }, | ||
| select: { | ||
| ...bookingMinimalSelect, | ||
| eventType: { | ||
| select: { | ||
| title: true, | ||
| }, | ||
| }, | ||
| user: { | ||
| select: { | ||
| id: true, | ||
| username: true, | ||
| }, | ||
| }, | ||
| attendees: { | ||
| select: { | ||
| name: true, | ||
| email: true, | ||
| phoneNumber: true, | ||
| }, | ||
| // Ascending order ensures that the first attendee in the list is the booker and others are guests | ||
| // See why it is important https://github.com/calcom/cal.com/pull/20935 | ||
| // TODO: Ideally we should return `booker` property directly from the booking | ||
|
Comment on lines
+414
to
+416
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. cc @joeauyeung made this change here |
||
| orderBy: { | ||
| id: "asc", | ||
| }, | ||
| }, | ||
| }, | ||
| }); | ||
| } | ||
|
|
||
| async findBookingForMeetingPage({ bookingUid }: { bookingUid: string }) { | ||
| return await this.prismaClient.booking.findUnique({ | ||
| where: { | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
💡 Verification agent
🧩 Analysis chain
Signature change: ensure all call sites updated to required bookingId
chargeCard now requires bookingId (no optional param). Verify interface/type and usages are updated accordingly to avoid compile/runtime breaks.
Run:
🏁 Script executed:
Length of output: 268
🏁 Script executed:
Length of output: 5205
Require bookingId parameter across interface and all implementations
bookingId?: Booking["id"]tobookingId: Booking["id"]async chargeCard(payment, bookingId)(payment, bookingId)to chargeCard signature🤖 Prompt for AI Agents