diff --git a/packages/backend/src/config/app.ts b/packages/backend/src/config/app.ts index c3814b7c..efa5e8fa 100644 --- a/packages/backend/src/config/app.ts +++ b/packages/backend/src/config/app.ts @@ -40,6 +40,7 @@ type AppConfig = { fromEmail: string; isCloud: boolean; stripeSecretKey: string; + stripeSigningSecret: string; stripeStarterPriceKey: string; stripeGrowthPriceKey: string; licenseKey: string; @@ -110,6 +111,7 @@ const appConfig: AppConfig = { fromEmail: process.env.FROM_EMAIL, isCloud: process.env.AUTOMATISCH_CLOUD === 'true', stripeSecretKey: process.env.STRIPE_SECRET_KEY, + stripeSigningSecret: process.env.STRIPE_SIGNING_SECRET, stripeStarterPriceKey: process.env.STRIPE_STARTER_PRICE_KEY, stripeGrowthPriceKey: process.env.STRIPE_GROWTH_PRICE_KEY, licenseKey: process.env.LICENSE_KEY, diff --git a/packages/backend/src/controllers/stripe/webhooks.ee.ts b/packages/backend/src/controllers/stripe/webhooks.ee.ts new file mode 100644 index 00000000..fedbaea3 --- /dev/null +++ b/packages/backend/src/controllers/stripe/webhooks.ee.ts @@ -0,0 +1,23 @@ +import { Response } from 'express'; +import { IRequest } from '@automatisch/types'; +import Billing from '../../helpers/billing/index.ee'; +import appConfig from '../../config/app'; +import logger from '../../helpers/logger'; + +export default async (request: IRequest, response: Response) => { + const signature = request.headers['stripe-signature']; + + try { + const event = Billing.stripe.webhooks.constructEvent( + request.rawBody, + signature, + appConfig.stripeSigningSecret + ); + + await Billing.handleWebhooks(event); + return response.sendStatus(200); + } catch (error) { + logger.error(`Webhook Error: ${error.message}`); + return response.sendStatus(400); + } +}; diff --git a/packages/backend/src/helpers/billing/index.ee.ts b/packages/backend/src/helpers/billing/index.ee.ts index 24e72f2a..56671078 100644 --- a/packages/backend/src/helpers/billing/index.ee.ts +++ b/packages/backend/src/helpers/billing/index.ee.ts @@ -3,6 +3,7 @@ import User from '../../models/user'; import PaymentPlan from '../../models/payment-plan.ee'; import UsageData from '../../models/usage-data.ee'; import appConfig from '../../config/app'; +import handleWebhooks from './webhooks.ee'; const plans = [ { @@ -91,6 +92,9 @@ const createPaymentPortalUrl = async (user: User) => { const billing = { createSubscription, createPaymentPortalUrl, + handleWebhooks, + stripe, + plans, }; export default billing; diff --git a/packages/backend/src/helpers/billing/webhooks.ee.ts b/packages/backend/src/helpers/billing/webhooks.ee.ts new file mode 100644 index 00000000..8ec18830 --- /dev/null +++ b/packages/backend/src/helpers/billing/webhooks.ee.ts @@ -0,0 +1,42 @@ +import Stripe from 'stripe'; +import PaymentPlan from '../../models/payment-plan.ee'; +import Billing from './index.ee'; + +const handleWebhooks = async (event: Stripe.Event) => { + const trackedWebhookTypes = [ + 'customer.subscription.created', + 'customer.subscription.updated', + 'customer.subscription.deleted', + ]; + + if (!trackedWebhookTypes.includes(event.type)) { + return; + } + + await updatePaymentPlan(event); +}; + +const updatePaymentPlan = async (event: Stripe.Event) => { + const subscription = event.data.object as Stripe.Subscription; + const priceKey = subscription.items.data[0].plan.id; + const plan = Billing.plans.find((plan) => plan.price === priceKey); + + const paymentPlan = await PaymentPlan.query().findOne({ + stripe_customer_id: subscription.customer, + }); + + await paymentPlan.$query().patchAndFetch({ + name: plan.name, + taskCount: plan.taskCount, + stripeSubscriptionId: subscription.id, + }); + + const user = await paymentPlan.$relatedQuery('user'); + const usageData = await user.$relatedQuery('usageData'); + + await usageData.$query().patchAndFetch({ + nextResetAt: new Date(subscription.current_period_end * 1000).toISOString(), + }); +}; + +export default handleWebhooks; diff --git a/packages/backend/src/models/payment-plan.ee.ts b/packages/backend/src/models/payment-plan.ee.ts index fdec5ed9..e492fb08 100644 --- a/packages/backend/src/models/payment-plan.ee.ts +++ b/packages/backend/src/models/payment-plan.ee.ts @@ -10,6 +10,7 @@ class PaymentPlan extends Base { stripeSubscriptionId!: string; currentPeriodStartedAt!: string; currentPeriodEndsAt!: string; + user?: User; static tableName = 'payment_plans'; diff --git a/packages/backend/src/routes/index.ts b/packages/backend/src/routes/index.ts index 1cb26316..853556ba 100644 --- a/packages/backend/src/routes/index.ts +++ b/packages/backend/src/routes/index.ts @@ -1,10 +1,12 @@ import { Router } from 'express'; import graphQLInstance from '../helpers/graphql-instance'; import webhooksRouter from './webhooks'; +import stripeRouter from './stripe.ee'; const router = Router(); router.use('/graphql', graphQLInstance); router.use('/webhooks', webhooksRouter); +router.use('/stripe', stripeRouter); export default router; diff --git a/packages/backend/src/routes/stripe.ee.ts b/packages/backend/src/routes/stripe.ee.ts new file mode 100644 index 00000000..f6c71aef --- /dev/null +++ b/packages/backend/src/routes/stripe.ee.ts @@ -0,0 +1,23 @@ +import express, { Router } from 'express'; +import multer from 'multer'; +import { IRequest } from '@automatisch/types'; +import appConfig from '../config/app'; +import stripeWebhooksAction from '../controllers/stripe/webhooks.ee'; + +const router = Router(); +const upload = multer(); + +router.use(upload.none()); + +router.use( + express.text({ + limit: appConfig.requestBodySizeLimit, + verify(req, res, buf) { + (req as IRequest).rawBody = buf; + }, + }) +); + +router.post('/webhooks', stripeWebhooksAction); + +export default router;