261 lines
7.6 KiB
TypeScript
261 lines
7.6 KiB
TypeScript
import { NextApiRequest, NextApiResponse } from 'next'
|
|
import { isDefined } from 'utils'
|
|
import {
|
|
badRequest,
|
|
forbidden,
|
|
methodNotAllowed,
|
|
notAuthenticated,
|
|
} from 'utils/api'
|
|
import Stripe from 'stripe'
|
|
import { withSentry } from '@sentry/nextjs'
|
|
import { getAuthenticatedUser } from '@/features/auth/api'
|
|
import prisma from '@/lib/prisma'
|
|
import { Plan, WorkspaceRole } from 'db'
|
|
|
|
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
|
const user = await getAuthenticatedUser(req)
|
|
if (!user) return notAuthenticated(res)
|
|
if (req.method === 'GET')
|
|
return res.send(await getSubscriptionDetails(req, res)(user.id))
|
|
if (req.method === 'POST') {
|
|
const session = await createCheckoutSession(req)
|
|
return res.send({ sessionId: session.id })
|
|
}
|
|
if (req.method === 'PUT') {
|
|
await updateSubscription(req)
|
|
return res.send({ message: 'success' })
|
|
}
|
|
if (req.method === 'DELETE') {
|
|
await cancelSubscription(req, res)(user.id)
|
|
return res.send({ message: 'success' })
|
|
}
|
|
return methodNotAllowed(res)
|
|
}
|
|
|
|
const getSubscriptionDetails =
|
|
(req: NextApiRequest, res: NextApiResponse) => async (userId: string) => {
|
|
const stripeId = req.query.stripeId as string | undefined
|
|
if (!stripeId) return badRequest(res)
|
|
if (!process.env.STRIPE_SECRET_KEY)
|
|
throw Error('STRIPE_SECRET_KEY var is missing')
|
|
const workspace = await prisma.workspace.findFirst({
|
|
where: {
|
|
stripeId,
|
|
members: { some: { userId, role: WorkspaceRole.ADMIN } },
|
|
},
|
|
})
|
|
if (!workspace?.stripeId) return forbidden(res)
|
|
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY, {
|
|
apiVersion: '2022-11-15',
|
|
})
|
|
const subscriptions = await stripe.subscriptions.list({
|
|
customer: workspace.stripeId,
|
|
limit: 1,
|
|
})
|
|
return {
|
|
additionalChatsIndex:
|
|
subscriptions.data[0]?.items.data.find(
|
|
(item) =>
|
|
item.price.id === process.env.STRIPE_ADDITIONAL_CHATS_PRICE_ID
|
|
)?.quantity ?? 0,
|
|
additionalStorageIndex:
|
|
subscriptions.data[0]?.items.data.find(
|
|
(item) =>
|
|
item.price.id === process.env.STRIPE_ADDITIONAL_STORAGE_PRICE_ID
|
|
)?.quantity ?? 0,
|
|
}
|
|
}
|
|
|
|
const createCheckoutSession = (req: NextApiRequest) => {
|
|
if (!process.env.STRIPE_SECRET_KEY)
|
|
throw Error('STRIPE_SECRET_KEY var is missing')
|
|
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY, {
|
|
apiVersion: '2022-11-15',
|
|
})
|
|
const {
|
|
email,
|
|
currency,
|
|
plan,
|
|
workspaceId,
|
|
href,
|
|
additionalChats,
|
|
additionalStorage,
|
|
} = typeof req.body === 'string' ? JSON.parse(req.body) : req.body
|
|
|
|
return stripe.checkout.sessions.create({
|
|
success_url: `${href}?stripe=${plan}&success=true`,
|
|
cancel_url: `${href}?stripe=cancel`,
|
|
allow_promotion_codes: true,
|
|
customer_email: email,
|
|
mode: 'subscription',
|
|
metadata: { workspaceId, plan, additionalChats, additionalStorage },
|
|
currency,
|
|
automatic_tax: { enabled: true },
|
|
line_items: parseSubscriptionItems(
|
|
plan,
|
|
additionalChats,
|
|
additionalStorage
|
|
),
|
|
})
|
|
}
|
|
|
|
const updateSubscription = async (req: NextApiRequest) => {
|
|
const {
|
|
stripeId,
|
|
plan,
|
|
workspaceId,
|
|
additionalChats,
|
|
additionalStorage,
|
|
currency,
|
|
} = (typeof req.body === 'string' ? JSON.parse(req.body) : req.body) as {
|
|
stripeId: string
|
|
workspaceId: string
|
|
additionalChats: number
|
|
additionalStorage: number
|
|
plan: 'STARTER' | 'PRO'
|
|
currency: 'eur' | 'usd'
|
|
}
|
|
if (!process.env.STRIPE_SECRET_KEY)
|
|
throw Error('STRIPE_SECRET_KEY var is missing')
|
|
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY, {
|
|
apiVersion: '2022-11-15',
|
|
})
|
|
const { data } = await stripe.subscriptions.list({
|
|
customer: stripeId,
|
|
})
|
|
const subscription = data[0] as Stripe.Subscription | undefined
|
|
const currentStarterPlanItemId = subscription?.items.data.find(
|
|
(item) => item.price.id === process.env.STRIPE_STARTER_PRICE_ID
|
|
)?.id
|
|
const currentProPlanItemId = subscription?.items.data.find(
|
|
(item) => item.price.id === process.env.STRIPE_PRO_PRICE_ID
|
|
)?.id
|
|
const currentAdditionalChatsItemId = subscription?.items.data.find(
|
|
(item) => item.price.id === process.env.STRIPE_ADDITIONAL_CHATS_PRICE_ID
|
|
)?.id
|
|
const currentAdditionalStorageItemId = subscription?.items.data.find(
|
|
(item) => item.price.id === process.env.STRIPE_ADDITIONAL_STORAGE_PRICE_ID
|
|
)?.id
|
|
const items = [
|
|
{
|
|
id: currentStarterPlanItemId ?? currentProPlanItemId,
|
|
price:
|
|
plan === Plan.STARTER
|
|
? process.env.STRIPE_STARTER_PRICE_ID
|
|
: process.env.STRIPE_PRO_PRICE_ID,
|
|
quantity: 1,
|
|
},
|
|
additionalChats === 0 && !currentAdditionalChatsItemId
|
|
? undefined
|
|
: {
|
|
id: currentAdditionalChatsItemId,
|
|
price: process.env.STRIPE_ADDITIONAL_CHATS_PRICE_ID,
|
|
quantity: additionalChats,
|
|
deleted: subscription ? additionalChats === 0 : undefined,
|
|
},
|
|
additionalStorage === 0 && !currentAdditionalStorageItemId
|
|
? undefined
|
|
: {
|
|
id: currentAdditionalStorageItemId,
|
|
price: process.env.STRIPE_ADDITIONAL_STORAGE_PRICE_ID,
|
|
quantity: additionalStorage,
|
|
deleted: subscription ? additionalStorage === 0 : undefined,
|
|
},
|
|
].filter(isDefined)
|
|
|
|
if (subscription) {
|
|
await stripe.subscriptions.update(subscription.id, {
|
|
items,
|
|
})
|
|
} else {
|
|
await stripe.subscriptions.create({
|
|
customer: stripeId,
|
|
items,
|
|
currency,
|
|
})
|
|
}
|
|
|
|
await prisma.workspace.update({
|
|
where: { id: workspaceId },
|
|
data: {
|
|
plan,
|
|
additionalChatsIndex: additionalChats,
|
|
additionalStorageIndex: additionalStorage,
|
|
chatsLimitFirstEmailSentAt: null,
|
|
chatsLimitSecondEmailSentAt: null,
|
|
storageLimitFirstEmailSentAt: null,
|
|
storageLimitSecondEmailSentAt: null,
|
|
},
|
|
})
|
|
}
|
|
|
|
const cancelSubscription =
|
|
(req: NextApiRequest, res: NextApiResponse) => async (userId: string) => {
|
|
const stripeId = req.query.stripeId as string | undefined
|
|
if (!stripeId) return badRequest(res)
|
|
if (!process.env.STRIPE_SECRET_KEY)
|
|
throw Error('STRIPE_SECRET_KEY var is missing')
|
|
const workspace = await prisma.workspace.findFirst({
|
|
where: {
|
|
stripeId,
|
|
members: { some: { userId, role: WorkspaceRole.ADMIN } },
|
|
},
|
|
})
|
|
if (!workspace?.stripeId) return forbidden(res)
|
|
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY, {
|
|
apiVersion: '2022-11-15',
|
|
})
|
|
const existingSubscription = await stripe.subscriptions.list({
|
|
customer: workspace.stripeId,
|
|
})
|
|
const currentSubscriptionId = existingSubscription.data[0]?.id
|
|
if (currentSubscriptionId)
|
|
await stripe.subscriptions.del(currentSubscriptionId)
|
|
|
|
await prisma.workspace.update({
|
|
where: { id: workspace.id },
|
|
data: {
|
|
plan: Plan.FREE,
|
|
additionalChatsIndex: 0,
|
|
additionalStorageIndex: 0,
|
|
},
|
|
})
|
|
}
|
|
|
|
const parseSubscriptionItems = (
|
|
plan: Plan,
|
|
additionalChats: number,
|
|
additionalStorage: number
|
|
) =>
|
|
[
|
|
{
|
|
price:
|
|
plan === Plan.STARTER
|
|
? process.env.STRIPE_STARTER_PRICE_ID
|
|
: process.env.STRIPE_PRO_PRICE_ID,
|
|
quantity: 1,
|
|
},
|
|
]
|
|
.concat(
|
|
additionalChats > 0
|
|
? [
|
|
{
|
|
price: process.env.STRIPE_ADDITIONAL_CHATS_PRICE_ID,
|
|
quantity: additionalChats,
|
|
},
|
|
]
|
|
: []
|
|
)
|
|
.concat(
|
|
additionalStorage > 0
|
|
? [
|
|
{
|
|
price: process.env.STRIPE_ADDITIONAL_STORAGE_PRICE_ID,
|
|
quantity: additionalStorage,
|
|
},
|
|
]
|
|
: []
|
|
)
|
|
|
|
export default withSentry(handler)
|