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" ;
2021-09-22 19:52:38 +00:00
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
2021-11-26 11:03:43 +00:00
import { sendAwaitingPaymentEmail , sendOrganizerPaymentRefundFailedEmail } from "@lib/emails/email-manager" ;
2021-09-22 19:52:38 +00:00
2021-11-26 11:03:43 +00:00
export type PaymentInfo = {
link? : string | null ;
reason? : string | null ;
id? : string | null ;
} ;
2021-09-22 18:36:13 +00:00
const paymentFeePercentage = process . env . PAYMENT_FEE_PERCENTAGE ! ;
const paymentFeeFixed = process . env . PAYMENT_FEE_FIXED ! ;
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 ;
}
) {
const paymentFee = Math . round (
2021-10-05 22:46:48 +00:00
selectedEventType . price * parseFloat ( ` ${ paymentFeePercentage } ` ) + parseInt ( ` ${ paymentFeeFixed } ` )
2021-09-22 18:36:13 +00:00
) ;
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 ( ) ,
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 ,
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" ,
} ) ;
}
}
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
}