164 lines
4.3 KiB
TypeScript
164 lines
4.3 KiB
TypeScript
import { PaymentType } from "@prisma/client";
|
|
import Stripe from "stripe";
|
|
import { JsonValue } from "type-fest";
|
|
import { v4 as uuidv4 } from "uuid";
|
|
|
|
import { CalendarEvent } from "@lib/calendarClient";
|
|
import EventOrganizerRefundFailedMail from "@lib/emails/EventOrganizerRefundFailedMail";
|
|
import EventPaymentMail from "@lib/emails/EventPaymentMail";
|
|
import { getErrorFromUnknown } from "@lib/errors";
|
|
import prisma from "@lib/prisma";
|
|
|
|
import { createPaymentLink } from "./client";
|
|
|
|
export type PaymentData = Stripe.Response<Stripe.PaymentIntent> & {
|
|
stripe_publishable_key: string;
|
|
stripeAccount: string;
|
|
};
|
|
|
|
export type StripeData = Stripe.OAuthToken & {
|
|
default_currency: string;
|
|
};
|
|
|
|
const stripePrivateKey = process.env.STRIPE_PRIVATE_KEY!;
|
|
const paymentFeePercentage = process.env.PAYMENT_FEE_PERCENTAGE!;
|
|
const paymentFeeFixed = process.env.PAYMENT_FEE_FIXED!;
|
|
|
|
const stripe = new Stripe(stripePrivateKey, {
|
|
apiVersion: "2020-08-27",
|
|
});
|
|
|
|
export async function handlePayment(
|
|
evt: CalendarEvent,
|
|
selectedEventType: {
|
|
price: number;
|
|
currency: string;
|
|
},
|
|
stripeCredential: { key: JsonValue },
|
|
booking: {
|
|
user: { email: string | null; name: string | null; timeZone: string } | null;
|
|
id: number;
|
|
startTime: { toISOString: () => string };
|
|
uid: string;
|
|
}
|
|
) {
|
|
const paymentFee = Math.round(
|
|
selectedEventType.price * parseFloat(`${paymentFeePercentage}`) + parseInt(`${paymentFeeFixed}`)
|
|
);
|
|
const { stripe_user_id, stripe_publishable_key } = stripeCredential.key as Stripe.OAuthToken;
|
|
|
|
const params: Stripe.PaymentIntentCreateParams = {
|
|
amount: selectedEventType.price,
|
|
currency: selectedEventType.currency,
|
|
payment_method_types: ["card"],
|
|
application_fee_amount: paymentFee,
|
|
};
|
|
|
|
const paymentIntent = await stripe.paymentIntents.create(params, { stripeAccount: stripe_user_id });
|
|
|
|
const payment = await prisma.payment.create({
|
|
data: {
|
|
type: PaymentType.STRIPE,
|
|
uid: uuidv4(),
|
|
bookingId: booking.id,
|
|
amount: selectedEventType.price,
|
|
fee: paymentFee,
|
|
currency: selectedEventType.currency,
|
|
success: false,
|
|
refunded: false,
|
|
data: Object.assign({}, paymentIntent, {
|
|
stripe_publishable_key,
|
|
stripeAccount: stripe_user_id,
|
|
}) as PaymentData as unknown as JsonValue,
|
|
externalId: paymentIntent.id,
|
|
},
|
|
});
|
|
|
|
const mail = new EventPaymentMail(
|
|
createPaymentLink({
|
|
paymentUid: payment.uid,
|
|
name: booking.user?.name,
|
|
date: booking.startTime.toISOString(),
|
|
}),
|
|
evt
|
|
);
|
|
await mail.sendEmail();
|
|
|
|
return payment;
|
|
}
|
|
|
|
export async function refund(
|
|
booking: {
|
|
id: number;
|
|
uid: string;
|
|
startTime: Date;
|
|
payment: {
|
|
id: number;
|
|
success: boolean;
|
|
refunded: boolean;
|
|
externalId: string;
|
|
data: JsonValue;
|
|
type: PaymentType;
|
|
}[];
|
|
},
|
|
calEvent: CalendarEvent
|
|
) {
|
|
try {
|
|
const payment = booking.payment.find((e) => e.success && !e.refunded);
|
|
if (!payment) return;
|
|
|
|
if (payment.type != PaymentType.STRIPE) {
|
|
await handleRefundError({
|
|
event: calEvent,
|
|
reason: "cannot refund non Stripe payment",
|
|
paymentId: "unknown",
|
|
});
|
|
return;
|
|
}
|
|
|
|
const refund = await stripe.refunds.create(
|
|
{
|
|
payment_intent: payment.externalId,
|
|
},
|
|
{ stripeAccount: (payment.data as unknown as PaymentData)["stripeAccount"] }
|
|
);
|
|
|
|
if (!refund || refund.status === "failed") {
|
|
await handleRefundError({
|
|
event: calEvent,
|
|
reason: refund?.failure_reason || "unknown",
|
|
paymentId: payment.externalId,
|
|
});
|
|
return;
|
|
}
|
|
|
|
await prisma.payment.update({
|
|
where: {
|
|
id: payment.id,
|
|
},
|
|
data: {
|
|
refunded: true,
|
|
},
|
|
});
|
|
} catch (e) {
|
|
const err = getErrorFromUnknown(e);
|
|
console.error(err, "Refund failed");
|
|
await handleRefundError({
|
|
event: calEvent,
|
|
reason: err.message || "unknown",
|
|
paymentId: "unknown",
|
|
});
|
|
}
|
|
}
|
|
|
|
async function handleRefundError(opts: { event: CalendarEvent; reason: string; paymentId: string }) {
|
|
console.error(`refund failed: ${opts.reason} for booking '${opts.event.uid}'`);
|
|
try {
|
|
await new EventOrganizerRefundFailedMail(opts.event, opts.reason, opts.paymentId).sendEmail();
|
|
} catch (e) {
|
|
console.error("Error while sending refund error email", e);
|
|
}
|
|
}
|
|
|
|
export default stripe;
|