171 lines
		
	
	
	
		
			4.5 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
			
		
		
	
	
			171 lines
		
	
	
	
		
			4.5 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
| import { PaymentType, Prisma } from "@prisma/client";
 | |
| import Stripe from "stripe";
 | |
| import { v4 as uuidv4 } from "uuid";
 | |
| 
 | |
| import { sendAwaitingPaymentEmail, sendOrganizerPaymentRefundFailedEmail } from "@lib/emails/email-manager";
 | |
| import { getErrorFromUnknown } from "@lib/errors";
 | |
| import { CalendarEvent } from "@lib/integrations/calendar/interfaces/Calendar";
 | |
| import prisma from "@lib/prisma";
 | |
| 
 | |
| import { createPaymentLink } from "./client";
 | |
| 
 | |
| export type PaymentInfo = {
 | |
|   link?: string | null;
 | |
|   reason?: string | null;
 | |
|   id?: string | null;
 | |
| };
 | |
| 
 | |
| 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: Prisma.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(),
 | |
|       booking: {
 | |
|         connect: {
 | |
|           id: 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,
 | |
|       }) /* We should treat this */ as PaymentData /* but Prisma doesn't know how to handle it, so it we treat it */ as unknown /* and then */ as Prisma.InputJsonValue,
 | |
|       externalId: paymentIntent.id,
 | |
|     },
 | |
|   });
 | |
| 
 | |
|   await sendAwaitingPaymentEmail({
 | |
|     ...evt,
 | |
|     paymentInfo: {
 | |
|       link: createPaymentLink({
 | |
|         paymentUid: payment.uid,
 | |
|         name: booking.user?.name,
 | |
|         date: booking.startTime.toISOString(),
 | |
|       }),
 | |
|     },
 | |
|   });
 | |
| 
 | |
|   return payment;
 | |
| }
 | |
| 
 | |
| export async function refund(
 | |
|   booking: {
 | |
|     id: number;
 | |
|     uid: string;
 | |
|     startTime: Date;
 | |
|     payment: {
 | |
|       id: number;
 | |
|       success: boolean;
 | |
|       refunded: boolean;
 | |
|       externalId: string;
 | |
|       data: Prisma.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}'`);
 | |
|   await sendOrganizerPaymentRefundFailedEmail({
 | |
|     ...opts.event,
 | |
|     paymentInfo: { reason: opts.reason, id: opts.paymentId },
 | |
|   });
 | |
| }
 | |
| 
 | |
| export default stripe;
 | 
