249 lines
8.2 KiB
TypeScript
249 lines
8.2 KiB
TypeScript
import { NextApiRequest, NextApiResponse } from 'next'
|
|
import { methodNotAllowed } from '@typebot.io/lib/api'
|
|
import Stripe from 'stripe'
|
|
import Cors from 'micro-cors'
|
|
import { buffer } from 'micro'
|
|
import prisma from '@/lib/prisma'
|
|
import { Plan, WorkspaceRole } from '@typebot.io/prisma'
|
|
import { RequestHandler } from 'next/dist/server/next'
|
|
import { sendTelemetryEvents } from '@typebot.io/lib/telemetry/sendTelemetryEvent'
|
|
import { Settings } from '@typebot.io/schemas'
|
|
import { env } from '@typebot.io/env'
|
|
|
|
if (!env.STRIPE_SECRET_KEY || !env.STRIPE_WEBHOOK_SECRET)
|
|
throw new Error('STRIPE_SECRET_KEY or STRIPE_WEBHOOK_SECRET missing')
|
|
const stripe = new Stripe(env.STRIPE_SECRET_KEY, {
|
|
apiVersion: '2022-11-15',
|
|
})
|
|
|
|
const cors = Cors({
|
|
allowMethods: ['POST', 'HEAD'],
|
|
})
|
|
|
|
const webhookSecret = env.STRIPE_WEBHOOK_SECRET as string
|
|
|
|
export const config = {
|
|
api: {
|
|
bodyParser: false,
|
|
},
|
|
}
|
|
|
|
const webhookHandler = async (req: NextApiRequest, res: NextApiResponse) => {
|
|
if (req.method === 'POST') {
|
|
const buf = await buffer(req)
|
|
const sig = req.headers['stripe-signature']
|
|
|
|
if (!sig) return res.status(400).send(`stripe-signature is missing`)
|
|
try {
|
|
const event = stripe.webhooks.constructEvent(
|
|
buf.toString(),
|
|
sig.toString(),
|
|
webhookSecret
|
|
)
|
|
switch (event.type) {
|
|
case 'checkout.session.completed': {
|
|
const session = event.data.object as Stripe.Checkout.Session
|
|
const metadata = session.metadata as unknown as
|
|
| {
|
|
plan: 'STARTER' | 'PRO'
|
|
additionalChats: string
|
|
additionalStorage: string
|
|
workspaceId: string
|
|
userId: string
|
|
}
|
|
| { claimableCustomPlanId: string; userId: string }
|
|
if ('plan' in metadata) {
|
|
const { workspaceId, plan, additionalChats, additionalStorage } =
|
|
metadata
|
|
if (!workspaceId || !plan || !additionalChats || !additionalStorage)
|
|
return res
|
|
.status(500)
|
|
.send({ message: `Couldn't retrieve valid metadata` })
|
|
const workspace = await prisma.workspace.update({
|
|
where: { id: workspaceId },
|
|
data: {
|
|
plan,
|
|
stripeId: session.customer as string,
|
|
additionalChatsIndex: parseInt(additionalChats),
|
|
additionalStorageIndex: parseInt(additionalStorage),
|
|
isQuarantined: false,
|
|
},
|
|
include: {
|
|
members: {
|
|
select: { user: { select: { id: true } } },
|
|
where: {
|
|
role: WorkspaceRole.ADMIN,
|
|
},
|
|
},
|
|
},
|
|
})
|
|
|
|
for (const user of workspace.members.map((member) => member.user)) {
|
|
if (!user?.id) continue
|
|
await sendTelemetryEvents([
|
|
{
|
|
name: 'Subscription updated',
|
|
workspaceId,
|
|
userId: user.id,
|
|
data: {
|
|
plan,
|
|
additionalChatsIndex: parseInt(additionalChats),
|
|
additionalStorageIndex: parseInt(additionalStorage),
|
|
},
|
|
},
|
|
])
|
|
}
|
|
} else {
|
|
const { claimableCustomPlanId, userId } = metadata
|
|
if (!claimableCustomPlanId)
|
|
return res
|
|
.status(500)
|
|
.send({ message: `Couldn't retrieve valid metadata` })
|
|
const { workspaceId, chatsLimit, seatsLimit, storageLimit } =
|
|
await prisma.claimableCustomPlan.update({
|
|
where: { id: claimableCustomPlanId },
|
|
data: { claimedAt: new Date() },
|
|
})
|
|
|
|
await prisma.workspace.updateMany({
|
|
where: { id: workspaceId },
|
|
data: {
|
|
plan: Plan.CUSTOM,
|
|
stripeId: session.customer as string,
|
|
customChatsLimit: chatsLimit,
|
|
customStorageLimit: storageLimit,
|
|
customSeatsLimit: seatsLimit,
|
|
},
|
|
})
|
|
|
|
await sendTelemetryEvents([
|
|
{
|
|
name: 'Subscription updated',
|
|
workspaceId,
|
|
userId,
|
|
data: {
|
|
plan: Plan.CUSTOM,
|
|
additionalChatsIndex: 0,
|
|
additionalStorageIndex: 0,
|
|
},
|
|
},
|
|
])
|
|
}
|
|
|
|
return res.status(200).send({ message: 'workspace upgraded in DB' })
|
|
}
|
|
case 'customer.subscription.deleted': {
|
|
const subscription = event.data.object as Stripe.Subscription
|
|
const { data } = await stripe.subscriptions.list({
|
|
customer: subscription.customer as string,
|
|
limit: 1,
|
|
status: 'active',
|
|
})
|
|
const existingSubscription = data[0] as
|
|
| Stripe.Subscription
|
|
| undefined
|
|
if (existingSubscription)
|
|
return res.send({
|
|
message:
|
|
'An active subscription still exists. Skipping downgrade.',
|
|
})
|
|
const workspace = await prisma.workspace.update({
|
|
where: {
|
|
stripeId: subscription.customer as string,
|
|
},
|
|
data: {
|
|
plan: Plan.FREE,
|
|
additionalChatsIndex: 0,
|
|
additionalStorageIndex: 0,
|
|
customChatsLimit: null,
|
|
customStorageLimit: null,
|
|
customSeatsLimit: null,
|
|
},
|
|
include: {
|
|
members: {
|
|
select: { user: { select: { id: true } } },
|
|
where: {
|
|
role: WorkspaceRole.ADMIN,
|
|
},
|
|
},
|
|
},
|
|
})
|
|
|
|
for (const user of workspace.members.map((member) => member.user)) {
|
|
if (!user?.id) continue
|
|
await sendTelemetryEvents([
|
|
{
|
|
name: 'Subscription updated',
|
|
workspaceId: workspace.id,
|
|
userId: user.id,
|
|
data: {
|
|
plan: Plan.FREE,
|
|
additionalChatsIndex: 0,
|
|
additionalStorageIndex: 0,
|
|
},
|
|
},
|
|
])
|
|
}
|
|
|
|
const typebots = await prisma.typebot.findMany({
|
|
where: {
|
|
workspaceId: workspace.id,
|
|
isArchived: { not: true },
|
|
},
|
|
include: { publishedTypebot: true },
|
|
})
|
|
for (const typebot of typebots) {
|
|
const settings = typebot.settings as Settings
|
|
if (settings.general.isBrandingEnabled) continue
|
|
await prisma.typebot.updateMany({
|
|
where: { id: typebot.id },
|
|
data: {
|
|
settings: {
|
|
...settings,
|
|
general: {
|
|
...settings.general,
|
|
isBrandingEnabled: true,
|
|
},
|
|
},
|
|
},
|
|
})
|
|
const publishedTypebotSettings = typebot.publishedTypebot
|
|
?.settings as Settings | null
|
|
if (
|
|
!publishedTypebotSettings ||
|
|
publishedTypebotSettings?.general.isBrandingEnabled
|
|
)
|
|
continue
|
|
await prisma.publicTypebot.updateMany({
|
|
where: { id: typebot.id },
|
|
data: {
|
|
settings: {
|
|
...publishedTypebotSettings,
|
|
general: {
|
|
...publishedTypebotSettings.general,
|
|
isBrandingEnabled: true,
|
|
},
|
|
},
|
|
},
|
|
})
|
|
}
|
|
return res.send({ message: 'workspace downgraded in DB' })
|
|
}
|
|
default: {
|
|
return res.status(304).send({ message: 'event not handled' })
|
|
}
|
|
}
|
|
} catch (err) {
|
|
console.error(err)
|
|
if (err instanceof Error) {
|
|
console.error(err)
|
|
return res.status(400).send(`Webhook Error: ${err.message}`)
|
|
}
|
|
return res.status(500).send(`Error occured: ${err}`)
|
|
}
|
|
}
|
|
return methodNotAllowed(res)
|
|
}
|
|
|
|
export default cors(webhookHandler as RequestHandler)
|