import { invalidateFQC } from '@matters/apollo-response-cache' ; import _ from 'lodash' ; import Stripe from 'stripe' ; import { CIRCLE_ACTION , CIRCLE_STATE , DB_NOTICE_TYPE , METADATA_KEY , NODE_TYPES , PAYMENT_CURRENCY , PAYMENT_PROVIDER , PRICE_STATE , SUBSCRIPTION_STATE , } from 'common/enums' ; import { environment } from 'common/environment' ; import { ServerError } from 'common/errors' ; import logger from 'common/logger' ; import { toDBAmount } from 'common/utils' ; import { AtomService , CacheService , NotificationService , PaymentService , } from 'connectors' ; import SlackService from 'connectors/slack' ; import { CirclePrice , CircleSubscription , Customer } from 'definitions' ; const stripe = new Stripe ( environment . stripeSecret , { apiVersion : '2020-08-27' , } ) ; param setupIntent param dbCustomer export const completeCircleSubscription = async ( { setupIntent , dbCustomer , event , } ) => { const atomService = new AtomService ( ) ; const paymentService = new PaymentService ( ) ; const notificationService = new NotificationService ( ) ; const slack = new SlackService ( ) ; const slackEventData = { id : event . id , type : event . type , } ; const metadata = setupIntent . metadata ; const userId = dbCustomer . userId ; const circleId = _ . get ( metadata , METADATA_KEY . CIRCLE_ID ) ; const priceId = _ . get ( metadata , METADATA_KEY . CIRCLE_PRICE_ID ) ; if ( ! circleId || ! priceId ) { return ; } const [ circle , price , customer ] = await Promise . all ( [ atomService . findFirst ( { table : 'circle' , where : { id : circleId , state : CIRCLE_STATE . active } , } ) , atomService . findFirst ( { table : 'circle_price' , where : { circleId , state : PRICE_STATE . active } , } ) , atomService . findFirst ( { table : 'customer' , where : { userId , provider : PAYMENT_PROVIDER . stripe , archived : false , } , } ) , ] ) ; if ( ! circle || ! price || ! customer ) { slack . sendStripeAlert ( { data : slackEventData , message : `can't find circle (${ circleId }), price (${ priceId }) or customer.` , } ) ; return ; } const subscriptions = await paymentService . findActiveSubscriptions ( { userId } ) ; await paymentService . createSubscriptionOrItem ( { userId , priceId : price . id , providerPriceId : price . providerPriceId , providerCustomerId : customer . customerId , subscriptions , } ) ; notificationService . trigger ( { event : DB_NOTICE_TYPE . circle_new_subscriber , actorId : userId , recipientId : circle . owner , entities : [ { type : 'target' , entityTable : 'circle' , entity : circle , } , ] , } ) ; const hasFollow = await atomService . count ( { table : 'action_circle' , where : { action : CIRCLE_ACTION . follow , userId , targetId : circleId , } , } ) ; if ( hasFollow === 0 ) { await atomService . create ( { table : 'action_circle' , data : { action : CIRCLE_ACTION . follow , userId , targetId : circleId , } , } ) ; } const cacheService = new CacheService ( ) ; invalidateFQC ( { node : { type : NODE_TYPES . Circle , id : circle . id } , redis : cacheService . redis , } ) ; invalidateFQC ( { node : { type : NODE_TYPES . User , id : userId } , redis : cacheService . redis , } ) ; } ; param subscription export const updateSubscription = async ( { subscription , event , } ) => { const atomService = new AtomService ( ) ; const cacheService = new CacheService ( ) ; const slack = new SlackService ( ) ; const slackEventData = { id : event . id , type : event . type , } ; const dbSub = await atomService . findFirst ( { table : 'circle_subscription' , where : { providerSubscriptionId : subscription . id } , } ) ; if ( ! dbSub ) { slack . sendStripeAlert ( { data : slackEventData , message : `can't find subscription ${ subscription . id }.` , } ) ; return ; } const isFromCanceled = dbSub . state === SUBSCRIPTION_STATE . canceled ; const isToCanceled = subscription . status === SUBSCRIPTION_STATE . canceled ; const isSubStateChanged = dbSub . state !== subscription . status ; const userId = dbSub . userId ; const subscriptionId = dbSub . id ; /**
     * update subscription status
     */ if ( isSubStateChanged && ! isFromCanceled ) { try { await atomService . update ( { table : 'circle_subscription' , where : { id : dbSub . id } , data : { state : subscription . status , canceledAt : subscription . canceled_at ? new Date ( subscription . canceled_at * 1000 ) : undefined , updatedAt : new Date ( ) , } , } ) ; } catch ( error ) { logger . error ( error ) ; throw new ServerError ( 'failed to update subscription' ) ; } } /**
     * archive subscription items if subscription becomes canceled
     */ let updatedPriceIds = null ; if ( isToCanceled ) { try { const updatedItems = await atomService . updateMany ( { table : 'circle_subscription_item' , where : { userId , subscriptionId , archived : false , provider : PAYMENT_PROVIDER . stripe , } , data : { archived : true , updatedAt : new Date ( ) , canceledAt : new Date ( ) , } , } ) ; updatedPriceIds = updatedItems . map ( ( i ) => i . priceId ) ; } catch ( error ) { logger . error ( error ) ; throw new ServerError ( 'failed to update subscription items' ) ; } } invalidateFQC ( { node : { type : NODE_TYPES . User , id : dbSub . userId } , redis : cacheService . redis , } ) ; if ( isToCanceled && updatedPriceIds ) { try { const dbDiffPrices = await atomService . findMany ( { table : 'circle_price' , whereIn : [ 'id' , updatedPriceIds ] , } ) ; dbDiffPrices . map ( ( price ) => { invalidateFQC ( { node : { type : NODE_TYPES . Circle , id : price . circleId } , redis : cacheService . redis , } ) ; } ) ; } catch ( error ) { logger . error ( error ) ; } } return ; } ; /**
 * handle circle invoice
 */ export const completeCircleInvoice = async ( { invoice , event , } ) => { const slack = new SlackService ( ) ; const slackEventData = { id : event . id , type : event . type , } ; logger . info ( `proceeding ${ event . type } event...` ) ; try { const atomService = new AtomService ( ) ; const paymentService = new PaymentService ( ) ; const providerInvoiceId = invoice . id as string ; const providerTxId = invoice . payment_intent as string ; const amount = toDBAmount ( { amount : invoice . amount_paid } ) ; const currency = _ . toUpper ( invoice . currency ) as PAYMENT_CURRENCY ; if ( ! providerTxId ) { return ; } const tx = ( await paymentService . findTransactions ( { providerTxId } ) ) [ 0 ] ; if ( tx ) { throw new ServerError ( `transaction already exists for invoice ${ providerInvoiceId }` ) ; } const customer = ( await atomService . findFirst ( { table : 'customer' , where : { customer_id : invoice . customer , provider : PAYMENT_PROVIDER . stripe , archived : false , } , } ) ) as Customer ; const subscription = ( await atomService . findFirst ( { table : 'circle_subscription' , where : { providerSubscriptionId : invoice . subscription , provider : PAYMENT_PROVIDER . stripe , } , } ) ) as CircleSubscription ; const lines = invoice . lines . has_more ? await stripe . invoices . listLineItems ( providerInvoiceId , { limit : 100 } ) : invoice . lines ; const providerPriceIds = lines . data . map ( ( item ) => item . price ? item . price . id : '' ) ; const prices = ( await atomService . findMany ( { table : 'circle_price' , whereIn : [ 'providerPriceId' , providerPriceIds ] , } ) ) as CirclePrice [ ] ; if ( customer && subscription && prices ) { await paymentService . createInvoiceWithTransactions ( { amount , currency , providerTxId , providerInvoiceId , subscriptionId : subscription . id , userId : customer . userId , prices , } ) ; } else { throw new ServerError ( `failed to complete invoice ${ providerInvoiceId }` ) ; } } catch ( error ) { logger . error ( error ) ; slack . sendStripeAlert ( { data : slackEventData , message : error , } ) ; throw error ; } } ;