chore: Handle stripe refunds slightly differently to reduce hard errors (#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
This commit is contained in:
@@ -6,7 +6,7 @@ import stripe from "@calcom/features/ee/payments/server/stripe";
|
||||
import { IS_PRODUCTION } from "@calcom/lib/constants";
|
||||
import { getErrorFromUnknown } from "@calcom/lib/errors";
|
||||
import { HttpError as HttpCode } from "@calcom/lib/http-error";
|
||||
import prisma from "@calcom/prisma";
|
||||
import { prisma } from "@calcom/prisma";
|
||||
|
||||
export const config = {
|
||||
api: {
|
||||
@@ -27,7 +27,10 @@ const handleSubscriptionUpdate = async (event: Stripe.Event) => {
|
||||
});
|
||||
|
||||
if (!app) {
|
||||
throw new HttpCode({ statusCode: 202, message: "Received and discarded" });
|
||||
throw new HttpCode({
|
||||
statusCode: 202,
|
||||
message: `No credential found with subscription ID ${subscription.id}`,
|
||||
});
|
||||
}
|
||||
|
||||
await prisma.credential.update({
|
||||
@@ -51,7 +54,10 @@ const handleSubscriptionDeleted = async (event: Stripe.Event) => {
|
||||
});
|
||||
|
||||
if (!app) {
|
||||
throw new HttpCode({ statusCode: 202, message: "Received and discarded" });
|
||||
throw new HttpCode({
|
||||
statusCode: 202,
|
||||
message: `No credential found with subscription ID ${subscription.id}`,
|
||||
});
|
||||
}
|
||||
|
||||
// should we delete the credential here rather than marking as inactive?
|
||||
@@ -103,7 +109,9 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||
}
|
||||
} catch (_err) {
|
||||
const err = getErrorFromUnknown(_err);
|
||||
console.error(`Webhook Error: ${err.message}`);
|
||||
if (!err.message.includes("No credential found with subscription ID")) {
|
||||
console.error(`Webhook Error: ${err.message}`);
|
||||
}
|
||||
res.status(err.statusCode ?? 500).send({
|
||||
message: err.message,
|
||||
stack: IS_PRODUCTION ? undefined : err.stack,
|
||||
|
||||
@@ -46,8 +46,14 @@ export class PaymentService implements IAbstractPaymentService {
|
||||
|
||||
private async getPayment(where: Prisma.PaymentWhereInput) {
|
||||
const payment = await prisma.payment.findFirst({ where });
|
||||
if (!payment) throw new Error("Payment not found");
|
||||
if (!payment.externalId) throw new Error("Payment externalId not found");
|
||||
// if payment isn't found, return null.
|
||||
if (!payment) {
|
||||
return null;
|
||||
}
|
||||
// if it is found, but there's no externalId - it indicates invalid state and an error should be thrown.
|
||||
if (!payment.externalId) {
|
||||
throw new Error("Payment externalId not found");
|
||||
}
|
||||
return { ...payment, externalId: payment.externalId };
|
||||
}
|
||||
|
||||
@@ -326,13 +332,21 @@ export class PaymentService implements IAbstractPaymentService {
|
||||
throw new Error("Method not implemented.");
|
||||
}
|
||||
|
||||
async refund(paymentId: Payment["id"]): Promise<Payment> {
|
||||
async refund(paymentId: Payment["id"]): Promise<Payment | null> {
|
||||
const payment = await this.getPayment({
|
||||
id: paymentId,
|
||||
});
|
||||
if (!payment) {
|
||||
return null;
|
||||
}
|
||||
if (!payment.success) {
|
||||
throw new Error("Unable to refund failed payment");
|
||||
}
|
||||
if (payment.refunded) {
|
||||
// refunded already, bail early as success without throwing an error.
|
||||
return payment;
|
||||
}
|
||||
try {
|
||||
const payment = await this.getPayment({
|
||||
id: paymentId,
|
||||
success: true,
|
||||
refunded: false,
|
||||
});
|
||||
const refund = await this.stripe.refunds.create(
|
||||
{
|
||||
payment_intent: payment.externalId,
|
||||
@@ -399,8 +413,12 @@ export class PaymentService implements IAbstractPaymentService {
|
||||
const payment = await this.getPayment({
|
||||
id: paymentId,
|
||||
});
|
||||
const stripeAccount = (payment.data as unknown as StripePaymentData).stripeAccount;
|
||||
// no payment found, return false.
|
||||
if (!payment) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const stripeAccount = (payment.data as unknown as StripePaymentData).stripeAccount;
|
||||
if (!stripeAccount) {
|
||||
throw new Error("Stripe account not found");
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ import z from "zod";
|
||||
|
||||
import { getCalendar } from "@calcom/app-store/_utils/getCalendar";
|
||||
import { appStoreMetadata } from "@calcom/app-store/appStoreMetaData";
|
||||
import { DailyLocationType } from "@calcom/app-store/locations";
|
||||
import {
|
||||
type EventTypeAppMetadataSchema,
|
||||
eventTypeAppMetadataOptionalSchema,
|
||||
@@ -14,7 +15,6 @@ import { deleteWebhookScheduledTriggers } from "@calcom/features/webhooks/lib/sc
|
||||
import { buildNonDelegationCredential } from "@calcom/lib/delegationCredential/server";
|
||||
import { isPrismaObjOrUndefined } from "@calcom/lib/isPrismaObj";
|
||||
import { parseRecurringEvent } from "@calcom/lib/isRecurringEvent";
|
||||
import { DailyLocationType } from "@calcom/app-store/locations";
|
||||
import { getTranslation } from "@calcom/lib/server/i18n";
|
||||
import { bookingMinimalSelect, prisma } from "@calcom/prisma";
|
||||
import type { Prisma } from "@calcom/prisma/client";
|
||||
@@ -284,11 +284,7 @@ const handleDeleteCredential = async ({
|
||||
});
|
||||
|
||||
for (const payment of booking.payment) {
|
||||
try {
|
||||
await deletePayment(payment.id, credential);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
await deletePayment(payment.id, credential);
|
||||
await prisma.payment.delete({
|
||||
where: {
|
||||
id: payment.id,
|
||||
@@ -353,7 +349,7 @@ const handleDeleteCredential = async ({
|
||||
seatsPerTimeSlot: booking.eventType?.seatsPerTimeSlot,
|
||||
seatsShowAttendees: booking.eventType?.seatsShowAttendees,
|
||||
hideOrganizerEmail: booking.eventType?.hideOrganizerEmail,
|
||||
team: !!booking.eventType?.team
|
||||
team: booking.eventType?.team
|
||||
? {
|
||||
name: booking.eventType.team.name,
|
||||
id: booking.eventType.team.id,
|
||||
|
||||
Vendored
+4
-1
@@ -39,7 +39,10 @@ export interface IAbstractPaymentService {
|
||||
): Promise<Payment>;
|
||||
|
||||
update(paymentId: Payment["id"], data: Partial<Prisma.PaymentUncheckedCreateInput>): Promise<Payment>;
|
||||
refund(paymentId: Payment["id"]): Promise<Payment>;
|
||||
/**
|
||||
* @returns Payment if successful, null if payment to refund does not exist (anymore)
|
||||
*/
|
||||
refund(paymentId: Payment["id"]): Promise<Payment | null>;
|
||||
getPaymentPaidStatus(): Promise<string>;
|
||||
getPaymentDetails(): Promise<Payment>;
|
||||
afterPayment(
|
||||
|
||||
Reference in New Issue
Block a user