2021-12-09 15:51:37 +00:00
import { PaymentType , Prisma } from "@prisma/client" ;
2021-09-22 18:36:13 +00:00
import Stripe from "stripe" ;
import { v4 as uuidv4 } from "uuid" ;
2022-05-26 16:01:12 +00:00
import { z } from "zod" ;
2021-09-22 19:52:38 +00:00
2022-05-11 04:58:10 +00:00
import getAppKeysFromSlug from "@calcom/app-store/_utils/getAppKeysFromSlug" ;
2022-06-06 17:49:56 +00:00
import { sendAwaitingPaymentEmail , sendOrganizerPaymentRefundFailedEmail } from "@calcom/emails" ;
2022-03-16 23:36:43 +00:00
import { getErrorFromUnknown } from "@calcom/lib/errors" ;
2022-03-03 19:29:19 +00:00
import prisma from "@calcom/prisma" ;
2022-03-09 22:56:05 +00:00
import { createPaymentLink } from "@calcom/stripe/client" ;
import stripe , { PaymentData } from "@calcom/stripe/server" ;
2022-03-23 22:00:30 +00:00
import { CalendarEvent } from "@calcom/types/Calendar" ;
2022-03-03 19:29:19 +00:00
2022-05-26 16:01:12 +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 ( ) ,
} ) ;
2021-09-22 18:36:13 +00:00
export async function handlePayment (
evt : CalendarEvent ,
selectedEventType : {
price : number ;
currency : string ;
} ,
2021-12-09 15:51:37 +00:00
stripeCredential : { key : Prisma.JsonValue } ,
2021-09-22 18:36:13 +00:00
booking : {
2021-10-05 22:46:48 +00:00
user : { email : string | null ; name : string | null ; timeZone : string } | null ;
2021-09-22 18:36:13 +00:00
id : number ;
startTime : { toISOString : ( ) = > string } ;
uid : string ;
}
) {
2022-05-11 04:58:10 +00:00
const appKeys = await getAppKeysFromSlug ( "stripe" ) ;
2022-05-26 16:01:12 +00:00
const { payment_fee_fixed , payment_fee_percentage } = stripeKeysSchema . parse ( appKeys ) ;
2022-05-11 04:58:10 +00:00
2022-05-26 16:01:12 +00:00
const paymentFee = Math . round ( selectedEventType . price * payment_fee_percentage + payment_fee_fixed ) ;
const { stripe_user_id , stripe_publishable_key } = stripeCredentialSchema . parse ( stripeCredential . key ) ;
2021-09-22 18:36:13 +00:00
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 ( ) ,
2021-12-17 16:58:23 +00:00
booking : {
connect : {
id : booking.id ,
} ,
} ,
2021-09-22 18:36:13 +00:00
amount : selectedEventType.price ,
fee : paymentFee ,
currency : selectedEventType.currency ,
success : false ,
refunded : false ,
data : Object.assign ( { } , paymentIntent , {
stripe_publishable_key ,
stripeAccount : stripe_user_id ,
2022-01-06 17:28:31 +00:00
} ) /* 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 ,
2021-09-22 18:36:13 +00:00
externalId : paymentIntent.id ,
} ,
} ) ;
2021-11-26 11:03:43 +00:00
await sendAwaitingPaymentEmail ( {
. . . evt ,
paymentInfo : {
link : createPaymentLink ( {
paymentUid : payment.uid ,
name : booking.user?.name ,
2022-05-25 01:29:29 +00:00
email : booking.user?.email ,
2021-11-26 11:03:43 +00:00
date : booking.startTime.toISOString ( ) ,
} ) ,
} ,
} ) ;
2021-09-22 18:36:13 +00:00
return payment ;
}
export async function refund (
booking : {
id : number ;
uid : string ;
startTime : Date ;
payment : {
id : number ;
success : boolean ;
refunded : boolean ;
externalId : string ;
2021-12-09 15:51:37 +00:00
data : Prisma.JsonValue ;
2021-09-22 18:36:13 +00:00
type : PaymentType ;
} [ ] ;
} ,
calEvent : CalendarEvent
) {
try {
const payment = booking . payment . find ( ( e ) = > e . success && ! e . refunded ) ;
if ( ! payment ) return ;
2021-12-09 15:51:37 +00:00
if ( payment . type !== PaymentType . STRIPE ) {
2021-09-22 18:36:13 +00:00
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 ) {
2021-10-28 22:58:26 +00:00
const err = getErrorFromUnknown ( e ) ;
console . error ( err , "Refund failed" ) ;
2021-09-22 18:36:13 +00:00
await handleRefundError ( {
event : calEvent ,
2021-10-28 22:58:26 +00:00
reason : err.message || "unknown" ,
2021-09-22 18:36:13 +00:00
paymentId : "unknown" ,
} ) ;
}
}
2022-06-20 17:52:50 +00:00
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 ;
}
} ;
2021-10-28 22:58:26 +00:00
async function handleRefundError ( opts : { event : CalendarEvent ; reason : string ; paymentId : string } ) {
console . error ( ` refund failed: ${ opts . reason } for booking ' ${ opts . event . uid } ' ` ) ;
2021-11-26 11:03:43 +00:00
await sendOrganizerPaymentRefundFailedEmail ( {
. . . opts . event ,
paymentInfo : { reason : opts.reason , id : opts.paymentId } ,
} ) ;
2021-09-22 18:36:13 +00:00
}