bf80347abf
* feat(workspaces): delete workspace emit event * feat(workspaces): move workspace group metrics to the backend * Removed FE mixpanel group update * Remove fragment * test(gatekeeper): add unittest to new gatekeeper service --------- Co-authored-by: Mike Tasset <mike.tasset@gmail.com>
157 lines
5.5 KiB
TypeScript
157 lines
5.5 KiB
TypeScript
import { Router } from 'express'
|
|
import { ensureError } from '@speckle/shared'
|
|
import { Stripe } from 'stripe'
|
|
import { getStripeEndpointSigningKey } from '@/modules/shared/helpers/envHelper'
|
|
import { db } from '@/db/knex'
|
|
import { completeCheckoutSessionFactory } from '@/modules/gatekeeper/services/checkout'
|
|
import {
|
|
getSubscriptionDataFactory,
|
|
parseSubscriptionData
|
|
} from '@/modules/gatekeeper/clients/stripe'
|
|
import {
|
|
deleteCheckoutSessionFactory,
|
|
getCheckoutSessionFactory,
|
|
getWorkspacePlanFactory,
|
|
upsertWorkspaceSubscriptionFactory,
|
|
updateCheckoutSessionStatusFactory,
|
|
upsertPaidWorkspacePlanFactory,
|
|
getWorkspaceSubscriptionBySubscriptionIdFactory
|
|
} from '@/modules/gatekeeper/repositories/billing'
|
|
import { WorkspaceAlreadyPaidError } from '@/modules/gatekeeper/errors/billing'
|
|
import { withTransaction } from '@/modules/shared/helpers/dbHelper'
|
|
import { getStripeClient } from '@/modules/gatekeeper/stripe'
|
|
import { handleSubscriptionUpdateFactory } from '@/modules/gatekeeper/services/subscriptions'
|
|
import { getEventBus } from '@/modules/shared/services/eventBus'
|
|
|
|
export const getBillingRouter = (): Router => {
|
|
const router = Router()
|
|
|
|
router.post('/api/v1/billing/webhooks', async (req, res) => {
|
|
const endpointSecret = getStripeEndpointSigningKey()
|
|
const sig = req.headers['stripe-signature']
|
|
if (!sig) {
|
|
res.status(400).send('Missing payload signature')
|
|
return
|
|
}
|
|
|
|
const stripe = getStripeClient()
|
|
let event: Stripe.Event
|
|
|
|
try {
|
|
event = stripe.webhooks.constructEvent(
|
|
// yes, the express json middleware auto parses the payload and stri need it in a string
|
|
req.body,
|
|
sig,
|
|
endpointSecret
|
|
)
|
|
} catch (err) {
|
|
res.status(400).send(`Webhook Error: ${ensureError(err).message}`)
|
|
return
|
|
}
|
|
|
|
switch (event.type) {
|
|
case 'checkout.session.async_payment_failed':
|
|
// if payment fails, we delete the failed session
|
|
await deleteCheckoutSessionFactory({ db })({
|
|
checkoutSessionId: event.data.object.id
|
|
})
|
|
break
|
|
case 'checkout.session.async_payment_succeeded':
|
|
case 'checkout.session.completed':
|
|
const session = event.data.object
|
|
|
|
if (!session.subscription)
|
|
return res.status(400).send('We only support subscription type checkouts')
|
|
|
|
switch (session.payment_status) {
|
|
case 'no_payment_required':
|
|
// we do not need to support this status
|
|
break
|
|
case 'paid':
|
|
// If the workspace is already on a paid plan, we made a bo bo.
|
|
// existing subs should be updated via the api, not pushed through the checkout sess again
|
|
// the start checkout endpoint should guard this!
|
|
// get checkout session from the DB, if not found CONTACT SUPPORT!!!
|
|
// if the session is already paid, means, we've already settled this checkout, and this is a webhook recall
|
|
// set checkout state to paid
|
|
// go ahead and provision the plan
|
|
// store customer id and subscription Id associated to the workspace plan
|
|
|
|
const subscriptionId =
|
|
typeof session.subscription === 'string'
|
|
? session.subscription
|
|
: session.subscription.id
|
|
|
|
// this must use a transaction
|
|
|
|
const trx = await db.transaction()
|
|
|
|
const completeCheckout = completeCheckoutSessionFactory({
|
|
getCheckoutSession: getCheckoutSessionFactory({ db: trx }),
|
|
updateCheckoutSessionStatus: updateCheckoutSessionStatusFactory({
|
|
db: trx
|
|
}),
|
|
upsertPaidWorkspacePlan: upsertPaidWorkspacePlanFactory({ db: trx }),
|
|
upsertWorkspaceSubscription: upsertWorkspaceSubscriptionFactory({
|
|
db: trx
|
|
}),
|
|
getSubscriptionData: getSubscriptionDataFactory({
|
|
stripe
|
|
}),
|
|
emitEvent: getEventBus().emit
|
|
})
|
|
|
|
try {
|
|
await withTransaction(
|
|
completeCheckout({
|
|
sessionId: session.id,
|
|
subscriptionId
|
|
}),
|
|
trx
|
|
)
|
|
} catch (err) {
|
|
if (err instanceof WorkspaceAlreadyPaidError) {
|
|
// ignore the request, this is prob a replay from stripe
|
|
} else {
|
|
throw err
|
|
}
|
|
}
|
|
|
|
break
|
|
case 'unpaid':
|
|
// if payment fails, we delete the failed session
|
|
await deleteCheckoutSessionFactory({ db })({
|
|
checkoutSessionId: event.data.object.id
|
|
})
|
|
}
|
|
break
|
|
|
|
case 'checkout.session.expired':
|
|
// delete the checkout session from the DB
|
|
await deleteCheckoutSessionFactory({ db })({
|
|
checkoutSessionId: event.data.object.id
|
|
})
|
|
break
|
|
|
|
case 'customer.subscription.updated':
|
|
case 'customer.subscription.deleted':
|
|
await handleSubscriptionUpdateFactory({
|
|
getWorkspacePlan: getWorkspacePlanFactory({ db }),
|
|
upsertPaidWorkspacePlan: upsertPaidWorkspacePlanFactory({ db }),
|
|
getWorkspaceSubscriptionBySubscriptionId:
|
|
getWorkspaceSubscriptionBySubscriptionIdFactory({ db }),
|
|
upsertWorkspaceSubscription: upsertWorkspaceSubscriptionFactory({ db })
|
|
})({ subscriptionData: parseSubscriptionData(event.data.object) })
|
|
|
|
break
|
|
|
|
default:
|
|
break
|
|
}
|
|
|
|
res.status(200).send('ok')
|
|
})
|
|
|
|
return router
|
|
}
|