2022-07-22 17:27:06 +00:00
import { PaymentType , Prisma } from "@prisma/client" ;
2022-03-09 22:56:05 +00:00
import Stripe from "stripe" ;
2022-07-22 17:27:06 +00:00
import { v4 as uuidv4 } from "uuid" ;
2022-06-20 19:09:22 +00:00
import { z } from "zod" ;
2022-03-09 22:56:05 +00:00
2022-07-22 17:27:06 +00:00
import getAppKeysFromSlug from "@calcom/app-store/_utils/getAppKeysFromSlug" ;
import { sendAwaitingPaymentEmail , sendOrganizerPaymentRefundFailedEmail } from "@calcom/emails" ;
import { getErrorFromUnknown } from "@calcom/lib/errors" ;
import prisma from "@calcom/prisma" ;
import type { CalendarEvent } from "@calcom/types/Calendar" ;
import { createPaymentLink } from "./client" ;
2022-03-09 22:56:05 +00:00
export type PaymentData = Stripe . Response < Stripe.PaymentIntent > & {
stripe_publishable_key : string ;
stripeAccount : string ;
} ;
2022-06-20 19:09:22 +00:00
export const stripeOAuthTokenSchema = z . object ( {
access_token : z.string ( ) . optional ( ) ,
scope : z.string ( ) . optional ( ) ,
livemode : z.boolean ( ) . optional ( ) ,
token_type : z.literal ( "bearer" ) . optional ( ) ,
refresh_token : z.string ( ) . optional ( ) ,
stripe_user_id : z.string ( ) . optional ( ) ,
stripe_publishable_key : z.string ( ) . optional ( ) ,
} ) ;
export const stripeDataSchema = stripeOAuthTokenSchema . extend ( {
default_currency : z.string ( ) ,
} ) ;
export type StripeData = z . infer < typeof stripeDataSchema > ;
2022-03-09 22:56:05 +00:00
const stripePrivateKey = process . env . STRIPE_PRIVATE_KEY ! ;
const stripe = new Stripe ( stripePrivateKey , {
apiVersion : "2020-08-27" ,
} ) ;
2022-07-22 17:27:06 +00:00
const stripeKeysSchema = z . object ( {
payment_fee_fixed : z.number ( ) ,
payment_fee_percentage : z.number ( ) ,
} ) ;
const stripeCredentialSchema = z . object ( {
stripe_user_id : z.string ( ) ,
stripe_publishable_key : z.string ( ) ,
} ) ;
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 appKeys = await getAppKeysFromSlug ( "stripe" ) ;
const { payment_fee_fixed , payment_fee_percentage } = stripeKeysSchema . parse ( appKeys ) ;
const paymentFee = Math . round ( selectedEventType . price * payment_fee_percentage + payment_fee_fixed ) ;
const { stripe_user_id , stripe_publishable_key } = stripeCredentialSchema . parse ( stripeCredential . key ) ;
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 ,
email : booking.user?.email ,
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" ,
} ) ;
}
}
export const closePayments = async ( paymentIntentId : string , stripeAccount : string ) = > {
try {
// Expire all current sessions
const sessions = await stripe . checkout . sessions . list (
{
payment_intent : paymentIntentId ,
} ,
{ stripeAccount }
) ;
for ( const session of sessions . data ) {
await stripe . checkout . sessions . expire ( session . id , { stripeAccount } ) ;
}
// Then cancel the payment intent
await stripe . paymentIntents . cancel ( paymentIntentId , { stripeAccount } ) ;
return ;
} catch ( e ) {
console . error ( e ) ;
return ;
}
} ;
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 } ,
} ) ;
}
2022-03-09 22:56:05 +00:00
export default stripe ;