UNPKG

@dbbs/strapi-stripe-payment

Version:
661 lines (576 loc) 19.3 kB
import Stripe from 'stripe' import { Strapi } from '@strapi/strapi' import createHttpError from 'http-errors' import { BillingReason, PaymentMode, PaymentTransactionStatus, PlanType, StripeEventType, SubscriptionStatus } from '../enums' import { Plan, Product } from '../interfaces' export default ({ strapi }: { strapi: Strapi }) => ({ async handleEvent(event: Stripe.Event) { switch (event.type) { case StripeEventType.CheckoutSessionCompleted: await this.handleCheckoutSessionCompleted(event) break case StripeEventType.InvoicePaymentSucceeded: await this.handleInvoicePaymentSucceeded(event) break case StripeEventType.InvoicePaymentFailed: await this.handleInvoicePaymentFailed(event) break case StripeEventType.CustomerSubscriptionUpdated: await this.handleSubscriptionUpdated(event) break case StripeEventType.PaymentMethodAttached: await this.handlePaymentMethodAttached(event) break case StripeEventType.PriceDeleted: await this.handlePriceDeleted(event) break case StripeEventType.PriceUpdated: await this.handlePriceUpdated(event) break case StripeEventType.PriceCreated: await this.handlePriceCreated(event) break case StripeEventType.ProductCreated: await this.handleProductCreated(event) break case StripeEventType.ProductUpdated: await this.handleProductUpdated(event) break case StripeEventType.ProductDeleted: await this.handleProductDeleted(event) break case StripeEventType.SetupIntentSucceeded: await this.handleSetupIntentSucceeded(event) break default: break } }, async handleCheckoutSessionCompleted(event: Stripe.CheckoutSessionCompletedEvent) { const checkoutSession = event.data.object if (checkoutSession.mode === PaymentMode.Setup) { return } try { const { metadata } = checkoutSession if (!metadata?.organizationName || !metadata?.userId || !metadata?.planId || !metadata?.quantity) { throw new createHttpError.BadRequest('Metadata is missing required fields') } const { organizationName, userId, planId, quantity } = metadata const isSubscription = checkoutSession.mode === PaymentMode.Subscription const stripeCustomerId = checkoutSession.customer const stripeSubscriptionId = checkoutSession.subscription const stripeInvoiceId = checkoutSession.invoice const stripePaymentIntentId = checkoutSession.payment_intent let stripeCustomer let stripeTransaction if (isSubscription) { stripeTransaction = await strapi.plugin('stripe-payment').service('stripe').invoices.retrieve(stripeInvoiceId) } else { stripeTransaction = await strapi .plugin('stripe-payment') .service('stripe') .paymentIntents.retrieve(stripePaymentIntentId) } const paymentTransaction = await strapi.query('plugin::stripe-payment.transaction').create({ data: { status: PaymentTransactionStatus.COMPLETED, externalTransaction: stripeTransaction } }) let organization = await strapi.query('plugin::stripe-payment.organization').findOne({ where: stripeCustomerId ? { customer_id: stripeCustomerId } : { name: organizationName }, populate: { subscription: true, transactions: true, purchases: true } }) if (!organization) { if (!stripeCustomerId) { const owner = await strapi.query('plugin::users-permissions.user').findOne({ where: { id: userId } }) stripeCustomer = await strapi.plugin('stripe-payment').service('stripe').customers.create({ name: organizationName, email: owner.email }) } organization = await strapi.query('plugin::stripe-payment.organization').create({ data: { name: organizationName, customer_id: stripeCustomer?.id ?? stripeCustomerId, owner_id: userId, users: [userId], quantity: parseInt(quantity, 10) } }) } if (isSubscription) { const subscription = await strapi.query('plugin::stripe-payment.subscription').create({ data: { status: SubscriptionStatus.TRIALING, stripe_id: stripeSubscriptionId, organization: organization.id, plan: planId } }) await strapi.query('plugin::stripe-payment.organization').update({ where: { id: organization.id }, data: { subscription: subscription.id, transactions: organization.transactions ? [...organization.transactions, paymentTransaction.id] : [paymentTransaction.id] } }) await strapi.query('plugin::stripe-payment.transaction').update({ where: { id: paymentTransaction.id }, data: { subscriptionId: subscription.id, organization: organization.id } }) } else { const purchase = await strapi.query('plugin::stripe-payment.purchase').create({ data: { plan: planId, organization: organization.id, stripe_id: stripeTransaction.id } }) await strapi.query('plugin::stripe-payment.organization').update({ where: { id: organization.id }, data: { purchases: organization.purchases ? [...organization.purchases, purchase.id] : [purchase.id], transactions: organization.transactions ? [...organization.transactions, paymentTransaction.id] : [paymentTransaction.id] } }) await strapi.query('plugin::stripe-payment.transaction').update({ where: { id: paymentTransaction.id }, data: { purchaseId: purchase.id, organization: organization.id } }) } } catch (e) { console.error(e) throw new Error('Could not create subscription or organization') } }, async handleInvoicePaymentSucceeded(event: Stripe.InvoicePaymentSucceededEvent) { if (event.data.object.billing_reason === BillingReason.Subscription_create) { return null } const subscription = await strapi.query('plugin::stripe-payment.subscription').findOne({ where: { stripe_id: event.data.object.subscription }, populate: { organization: true } }) if (!subscription) { throw new createHttpError.NotFound(`Subscription with stripe id ${event.data.object.subscription} was not found`) } if (subscription.status === SubscriptionStatus.ACTIVE) { return subscription } await strapi.query('plugin::stripe-payment.transaction').create({ data: { subscriptionId: subscription.id, organization: subscription.organization, status: PaymentTransactionStatus.COMPLETED, externalTransaction: event.data.object } }) return strapi.query('plugin::stripe-payment.subscription').update({ where: { id: subscription.id }, data: { status: SubscriptionStatus.ACTIVE } }) }, async handleInvoicePaymentFailed(event: Stripe.InvoicePaymentFailedEvent) { if (event.data.object.billing_reason === BillingReason.Subscription_create) { return null } const subscription = await strapi.query('plugin::stripe-payment.subscription').findOne({ where: { stripe_id: event.data.object.subscription }, populate: { organization: true } }) if (!subscription) { throw new createHttpError.NotFound(`Subscription with stripe id ${event.data.object.subscription} was not found`) } if (subscription.status === SubscriptionStatus.UNPAID) { return subscription } await strapi.query('plugin::stripe-payment.transaction').create({ data: { subscriptionId: subscription.id, organization: subscription.organization, status: PaymentTransactionStatus.COMPLETED, externalTransaction: event.data.object } }) return strapi.query('plugin::stripe-payment.subscription').update({ where: { id: subscription.id }, data: { status: SubscriptionStatus.UNPAID } }) }, async handleSubscriptionUpdated(event: Stripe.CustomerSubscriptionUpdatedEvent): Promise<void> { if (event.data.object.status === SubscriptionStatus.TRIALING) { return } const organization = await strapi.query('plugin::stripe-payment.organization').findOne({ where: { customer_id: event.data.object.customer } }) const { quantity, price } = event.data.object.items.data[0] if (!organization) { return } if (quantity && quantity > organization.quantity) { await strapi.query('plugin::stripe-payment.organization').update({ where: { id: organization.id }, data: { quantity } }) } if (price) { const plan = await strapi.query('plugin::stripe-payment.plan').findOne({ where: { stripe_id: price.id } }) if (plan) { await strapi.query('plugin::stripe-payment.subscription').update({ where: { id: event.data.object.id }, data: { plan: plan.id, status: event.data.object.status } }) } } }, async handlePaymentMethodAttached(event: Stripe.PaymentMethodAttachedEvent): Promise<void> { const customerId = event.data.object.customer const paymentMethodId = event.data.object.id await strapi .plugin('stripe-payment') .service('stripe') .customers.update(customerId, { invoice_settings: { default_payment_method: paymentMethodId } }) const organization = await strapi.query('plugin::stripe-payment.organization').findOne({ where: { customer_id: customerId } }) if (!organization) { await strapi.query('plugin::stripe-payment.organization').create({ data: { customer_id: customerId, payment_method_id: paymentMethodId } }) } else { await strapi.query('plugin::stripe-payment.organization').update({ where: { customer_id: customerId }, data: { payment_method_id: paymentMethodId } }) } }, async handlePriceDeleted(event: Stripe.PriceDeletedEvent) { const plan = await strapi.query('plugin::stripe-payment.plan').findOne({ where: { stripe_id: event.data.object.id } }) if (!plan) { throw new createHttpError.NotFound(`Plan with stripe id ${event.data.object.id} was not found`) } await strapi.query('plugin::stripe-payment.plan').delete({ where: { id: plan.id } }) }, async handlePriceUpdated(event: Stripe.PriceUpdatedEvent) { const existedPlan = await strapi.query('plugin::stripe-payment.plan').findOne({ where: { stripe_id: event.data.object.id } }) if (!existedPlan) { throw new createHttpError.NotFound(`Plan with stripe id ${event.data.object.id} was not found`) } const product = await strapi.query('plugin::stripe-payment.product').findOne({ where: { stripe_id: event.data.object.product }, populate: { plans: true } }) if (!product) { throw new createHttpError.NotFound(`Product with stripe id ${event.data.object.product} was not found`) } const planInterval = event.data.object.recurring?.interval const updatedPlan = await strapi.query('plugin::stripe-payment.plan').update({ where: { stripe_id: event.data.object.id }, data: { price: (event.data.object.unit_amount as number) / 100, interval: planInterval || null, stripe_id: event.data.object.id, currency: event.data.object.currency, type: planInterval ? PlanType.RECURRING : PlanType.ONE_TIME, product: product.id } }) const newPlansList = product.plans.filter((plan: Plan) => plan.stripe_id !== event.data.object.id) if (event.data.object.active) { newPlansList.push(updatedPlan) } await strapi.query('plugin::stripe-payment.product').update({ where: { id: product.id }, data: { plans: newPlansList } }) }, async handlePriceCreated(event: Stripe.PriceCreatedEvent): Promise<void> { const plan = await strapi.query('plugin::stripe-payment.plan').findOne({ where: { stripe_id: event.data.object.id } }) if (plan) { return } const product = await strapi.query('plugin::stripe-payment.product').findOne({ where: { stripe_id: event.data.object.product }, populate: { plans: true } }) if (!product) { throw new createHttpError.NotFound(`Product with stripe id ${event.data.object.product} was not found`) } const planInterval = event.data.object.recurring?.interval const savedPlan = await strapi.query('plugin::stripe-payment.plan').create({ data: { price: (event.data.object.unit_amount as number) / 100, interval: planInterval, stripe_id: event.data.object.id, currency: event.data.object.currency, type: planInterval ? PlanType.RECURRING : PlanType.ONE_TIME, product: product.id } }) await strapi.query('plugin::stripe-payment.product').update({ where: { stripe_id: event.data.object.product }, data: { plans: [...product.plans, savedPlan.id] } }) }, async handleProductCreated(event: Stripe.ProductCreatedEvent): Promise<void> { const product = await strapi.query('plugin::stripe-payment.product').findOne({ where: { stripe_id: event.data.object.id } }) if (product) { return } const savedProduct = await strapi.query('plugin::stripe-payment.product').create({ data: { name: event.data.object.name, stripe_id: event.data.object.id } }) const prices = await strapi .plugin('stripe-payment') .service('stripe') .prices.list({ product: event.data.object.id }) const price = prices.data[0] let savedPlan = await strapi.query('plugin::stripe-payment.plan').findOne({ where: { stripe_id: price.id } }) if (!savedPlan) { const planInterval = price.recurring?.interval savedPlan = await strapi.query('plugin::stripe-payment.plan').create({ data: { price: price.unit_amount / 100, interval: planInterval, stripe_id: price.id, currency: price.currency, type: planInterval ? PlanType.RECURRING : PlanType.ONE_TIME, product: savedProduct.id } }) } await strapi.query('plugin::stripe-payment.product').update({ where: { id: savedProduct.id }, data: { plans: [savedPlan.id] } }) }, async deleteProduct(product: Product) { const planStripeIds = product.plans.map((plan: Plan) => plan.stripe_id) await strapi.query('plugin::stripe-payment.plan').deleteMany({ where: { stripe_id: planStripeIds } }) await strapi.query('plugin::stripe-payment.product').delete({ where: { id: product.id } }) }, async handleProductUpdated(event: Stripe.ProductUpdatedEvent) { const product = await strapi.query('plugin::stripe-payment.product').findOne({ where: { stripe_id: event.data.object.id }, populate: { plans: true } }) if (!product) { throw new createHttpError.NotFound(`Product with stripe id ${event.data.object.id} was not found`) } const isActive = event.data.object.active if (!isActive) { await this.deleteProduct(product) return } const prices = await strapi .plugin('stripe-payment') .service('stripe') .prices.list({ product: event.data.object.id }) const existingPlans = product.plans.map((plan: Plan) => plan.stripe_id) const newPlansData: Plan[] = [] prices.data.forEach((price) => { if (!existingPlans.includes(price.id)) { const planInterval = price.recurring?.interval newPlansData.push({ price: price.unit_amount / 100, interval: planInterval, stripe_id: price.id, currency: price.currency, type: planInterval ? PlanType.RECURRING : PlanType.ONE_TIME, product: product.id }) } }) if (newPlansData.length > 0) { const newPlans = await strapi.query('plugin::stripe-payment.plan').createMany({ data: newPlansData }) await strapi.query('plugin::stripe-payment.product').update({ where: { id: product.id }, data: { name: event.data.object.name, plans: [...product.plans, ...newPlans.ids] }, populate: { plans: true } }) } else { await strapi.query('plugin::stripe-payment.product').update({ where: { id: product.id }, data: { name: event.data.object.name } }) } }, async handleProductDeleted(event: Stripe.ProductDeletedEvent) { const product = await strapi.query('plugin::stripe-payment.product').findOne({ where: { stripe_id: event.data.object.id }, populate: { plans: true } }) if (!product) { throw new createHttpError.NotFound(`Product with stripe id ${event.data.object.id} was not found`) } await this.deleteProduct(product) }, // TODO (#1113): Wrap payment method update with transaction async handleSetupIntentSucceeded(event: Stripe.SetupIntentSucceededEvent) { const setupIntent = event.data.object const organization = await strapi .query('plugin::stripe-payment.organization') .findOne({ where: { customer_id: setupIntent.customer } }) if (!organization) { throw new createHttpError.NotFound(`Organization with customer_id ${setupIntent.customer} was not found`) } const newPaymentMethod = await strapi .plugin('stripe-payment') .service('stripe') .paymentMethods.attach(setupIntent.payment_method, { customer: setupIntent.customer }) await strapi .plugin('stripe-payment') .service('stripe') .customers.update(organization.customer_id, { invoice_settings: { default_payment_method: newPaymentMethod.id } }) await strapi.query('plugin::stripe-payment.organization').update({ where: { id: organization.id }, data: { payment_method_id: newPaymentMethod.id } }) } })