Skip to content

Commit 259e480

Browse files
authored
chore: Handle stripe refunds slightly differently to reduce hard errors (calcom#24150)
* chore: Handle stripe refunds slightly differently to reduce hard errors * Remove 'Received and discarded' error when a credential is not found for a triggered subscription * Fix handling of no credential found (do show in stripe debugger) * Minimal overhaul of delete handling * Fix doc typo
1 parent 0055dd1 commit 259e480

4 files changed

Lines changed: 46 additions & 21 deletions

File tree

apps/web/pages/api/integrations/subscriptions/webhook.ts

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import stripe from "@calcom/features/ee/payments/server/stripe";
66
import { IS_PRODUCTION } from "@calcom/lib/constants";
77
import { getErrorFromUnknown } from "@calcom/lib/errors";
88
import { HttpError as HttpCode } from "@calcom/lib/http-error";
9-
import prisma from "@calcom/prisma";
9+
import { prisma } from "@calcom/prisma";
1010

1111
export const config = {
1212
api: {
@@ -27,7 +27,10 @@ const handleSubscriptionUpdate = async (event: Stripe.Event) => {
2727
});
2828

2929
if (!app) {
30-
throw new HttpCode({ statusCode: 202, message: "Received and discarded" });
30+
throw new HttpCode({
31+
statusCode: 202,
32+
message: `No credential found with subscription ID ${subscription.id}`,
33+
});
3134
}
3235

3336
await prisma.credential.update({
@@ -51,7 +54,10 @@ const handleSubscriptionDeleted = async (event: Stripe.Event) => {
5154
});
5255

5356
if (!app) {
54-
throw new HttpCode({ statusCode: 202, message: "Received and discarded" });
57+
throw new HttpCode({
58+
statusCode: 202,
59+
message: `No credential found with subscription ID ${subscription.id}`,
60+
});
5561
}
5662

5763
// should we delete the credential here rather than marking as inactive?
@@ -103,7 +109,9 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
103109
}
104110
} catch (_err) {
105111
const err = getErrorFromUnknown(_err);
106-
console.error(`Webhook Error: ${err.message}`);
112+
if (!err.message.includes("No credential found with subscription ID")) {
113+
console.error(`Webhook Error: ${err.message}`);
114+
}
107115
res.status(err.statusCode ?? 500).send({
108116
message: err.message,
109117
stack: IS_PRODUCTION ? undefined : err.stack,

packages/app-store/stripepayment/lib/PaymentService.ts

Lines changed: 27 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -46,8 +46,14 @@ export class PaymentService implements IAbstractPaymentService {
4646

4747
private async getPayment(where: Prisma.PaymentWhereInput) {
4848
const payment = await prisma.payment.findFirst({ where });
49-
if (!payment) throw new Error("Payment not found");
50-
if (!payment.externalId) throw new Error("Payment externalId not found");
49+
// if payment isn't found, return null.
50+
if (!payment) {
51+
return null;
52+
}
53+
// if it is found, but there's no externalId - it indicates invalid state and an error should be thrown.
54+
if (!payment.externalId) {
55+
throw new Error("Payment externalId not found");
56+
}
5157
return { ...payment, externalId: payment.externalId };
5258
}
5359

@@ -326,13 +332,21 @@ export class PaymentService implements IAbstractPaymentService {
326332
throw new Error("Method not implemented.");
327333
}
328334

329-
async refund(paymentId: Payment["id"]): Promise<Payment> {
335+
async refund(paymentId: Payment["id"]): Promise<Payment | null> {
336+
const payment = await this.getPayment({
337+
id: paymentId,
338+
});
339+
if (!payment) {
340+
return null;
341+
}
342+
if (!payment.success) {
343+
throw new Error("Unable to refund failed payment");
344+
}
345+
if (payment.refunded) {
346+
// refunded already, bail early as success without throwing an error.
347+
return payment;
348+
}
330349
try {
331-
const payment = await this.getPayment({
332-
id: paymentId,
333-
success: true,
334-
refunded: false,
335-
});
336350
const refund = await this.stripe.refunds.create(
337351
{
338352
payment_intent: payment.externalId,
@@ -399,8 +413,12 @@ export class PaymentService implements IAbstractPaymentService {
399413
const payment = await this.getPayment({
400414
id: paymentId,
401415
});
402-
const stripeAccount = (payment.data as unknown as StripePaymentData).stripeAccount;
416+
// no payment found, return false.
417+
if (!payment) {
418+
return false;
419+
}
403420

421+
const stripeAccount = (payment.data as unknown as StripePaymentData).stripeAccount;
404422
if (!stripeAccount) {
405423
throw new Error("Stripe account not found");
406424
}

packages/features/credentials/handleDeleteCredential.ts

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import z from "zod";
22

33
import { getCalendar } from "@calcom/app-store/_utils/getCalendar";
44
import { appStoreMetadata } from "@calcom/app-store/appStoreMetaData";
5+
import { DailyLocationType } from "@calcom/app-store/locations";
56
import {
67
type EventTypeAppMetadataSchema,
78
eventTypeAppMetadataOptionalSchema,
@@ -14,7 +15,6 @@ import { deleteWebhookScheduledTriggers } from "@calcom/features/webhooks/lib/sc
1415
import { buildNonDelegationCredential } from "@calcom/lib/delegationCredential/server";
1516
import { isPrismaObjOrUndefined } from "@calcom/lib/isPrismaObj";
1617
import { parseRecurringEvent } from "@calcom/lib/isRecurringEvent";
17-
import { DailyLocationType } from "@calcom/app-store/locations";
1818
import { getTranslation } from "@calcom/lib/server/i18n";
1919
import { bookingMinimalSelect, prisma } from "@calcom/prisma";
2020
import type { Prisma } from "@calcom/prisma/client";
@@ -284,11 +284,7 @@ const handleDeleteCredential = async ({
284284
});
285285

286286
for (const payment of booking.payment) {
287-
try {
288-
await deletePayment(payment.id, credential);
289-
} catch (e) {
290-
console.error(e);
291-
}
287+
await deletePayment(payment.id, credential);
292288
await prisma.payment.delete({
293289
where: {
294290
id: payment.id,
@@ -353,7 +349,7 @@ const handleDeleteCredential = async ({
353349
seatsPerTimeSlot: booking.eventType?.seatsPerTimeSlot,
354350
seatsShowAttendees: booking.eventType?.seatsShowAttendees,
355351
hideOrganizerEmail: booking.eventType?.hideOrganizerEmail,
356-
team: !!booking.eventType?.team
352+
team: booking.eventType?.team
357353
? {
358354
name: booking.eventType.team.name,
359355
id: booking.eventType.team.id,

packages/types/PaymentService.d.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,10 @@ export interface IAbstractPaymentService {
3939
): Promise<Payment>;
4040

4141
update(paymentId: Payment["id"], data: Partial<Prisma.PaymentUncheckedCreateInput>): Promise<Payment>;
42-
refund(paymentId: Payment["id"]): Promise<Payment>;
42+
/**
43+
* @returns Payment if successful, null if payment to refund does not exist (anymore)
44+
*/
45+
refund(paymentId: Payment["id"]): Promise<Payment | null>;
4346
getPaymentPaidStatus(): Promise<string>;
4447
getPaymentDetails(): Promise<Payment>;
4548
afterPayment(

0 commit comments

Comments
 (0)