-
-
Notifications
You must be signed in to change notification settings - Fork 2.3k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
♻️ (billing) Refactor billing server code to trpc
- Loading branch information
1 parent
9624387
commit b73282d
Showing
38 changed files
with
1,565 additions
and
367 deletions.
There are no files selected for viewing
70 changes: 70 additions & 0 deletions
70
apps/builder/src/features/billing/api/procedures/cancelSubscription.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,70 @@ | ||
import prisma from '@/lib/prisma' | ||
import { authenticatedProcedure } from '@/utils/server/trpc' | ||
import { TRPCError } from '@trpc/server' | ||
import { Plan, WorkspaceRole } from 'db' | ||
import Stripe from 'stripe' | ||
import { z } from 'zod' | ||
|
||
export const cancelSubscription = authenticatedProcedure | ||
.meta({ | ||
openapi: { | ||
method: 'DELETE', | ||
path: '/billing/subscription', | ||
protect: true, | ||
summary: 'Cancel current subscription', | ||
tags: ['Billing'], | ||
}, | ||
}) | ||
.input( | ||
z.object({ | ||
workspaceId: z.string(), | ||
}) | ||
) | ||
.output( | ||
z.object({ | ||
message: z.literal('success'), | ||
}) | ||
) | ||
.mutation(async ({ input: { workspaceId }, ctx: { user } }) => { | ||
if ( | ||
!process.env.STRIPE_SECRET_KEY || | ||
!process.env.STRIPE_ADDITIONAL_CHATS_PRICE_ID || | ||
!process.env.STRIPE_ADDITIONAL_STORAGE_PRICE_ID | ||
) | ||
throw new TRPCError({ | ||
code: 'INTERNAL_SERVER_ERROR', | ||
message: 'Stripe environment variables are missing', | ||
}) | ||
const workspace = await prisma.workspace.findFirst({ | ||
where: { | ||
id: workspaceId, | ||
members: { some: { userId: user.id, role: WorkspaceRole.ADMIN } }, | ||
}, | ||
}) | ||
if (!workspace?.stripeId) | ||
throw new TRPCError({ | ||
code: 'NOT_FOUND', | ||
message: 'Workspace not found', | ||
}) | ||
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY, { | ||
apiVersion: '2022-11-15', | ||
}) | ||
const currentSubscriptionId = ( | ||
await stripe.subscriptions.list({ | ||
customer: workspace.stripeId, | ||
}) | ||
).data.shift()?.id | ||
if (currentSubscriptionId) | ||
await stripe.subscriptions.del(currentSubscriptionId) | ||
|
||
await prisma.workspace.update({ | ||
where: { id: workspace.id }, | ||
data: { | ||
plan: Plan.FREE, | ||
additionalChatsIndex: 0, | ||
additionalStorageIndex: 0, | ||
}, | ||
}) | ||
|
||
return { message: 'success' } | ||
}) |
98 changes: 98 additions & 0 deletions
98
apps/builder/src/features/billing/api/procedures/createCheckoutSession.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,98 @@ | ||
import prisma from '@/lib/prisma' | ||
import { authenticatedProcedure } from '@/utils/server/trpc' | ||
import { TRPCError } from '@trpc/server' | ||
import { Plan, WorkspaceRole } from 'db' | ||
import Stripe from 'stripe' | ||
import { z } from 'zod' | ||
import { parseSubscriptionItems } from '../utils/parseSubscriptionItems' | ||
|
||
export const createCheckoutSession = authenticatedProcedure | ||
.meta({ | ||
openapi: { | ||
method: 'POST', | ||
path: '/billing/subscription/checkout', | ||
protect: true, | ||
summary: 'Create checkout session to create a new subscription', | ||
tags: ['Billing'], | ||
}, | ||
}) | ||
.input( | ||
z.object({ | ||
workspaceId: z.string(), | ||
prefilledEmail: z.string().optional(), | ||
currency: z.enum(['usd', 'eur']), | ||
plan: z.enum([Plan.STARTER, Plan.PRO]), | ||
returnUrl: z.string(), | ||
additionalChats: z.number(), | ||
additionalStorage: z.number(), | ||
}) | ||
) | ||
.output( | ||
z.object({ | ||
checkoutUrl: z.string(), | ||
}) | ||
) | ||
.mutation( | ||
async ({ | ||
input: { | ||
workspaceId, | ||
prefilledEmail, | ||
currency, | ||
plan, | ||
returnUrl, | ||
additionalChats, | ||
additionalStorage, | ||
}, | ||
ctx: { user }, | ||
}) => { | ||
if ( | ||
!process.env.STRIPE_SECRET_KEY || | ||
!process.env.STRIPE_ADDITIONAL_CHATS_PRICE_ID || | ||
!process.env.STRIPE_ADDITIONAL_STORAGE_PRICE_ID | ||
) | ||
throw new TRPCError({ | ||
code: 'INTERNAL_SERVER_ERROR', | ||
message: 'Stripe environment variables are missing', | ||
}) | ||
const workspace = await prisma.workspace.findFirst({ | ||
where: { | ||
id: workspaceId, | ||
members: { some: { userId: user.id, role: WorkspaceRole.ADMIN } }, | ||
}, | ||
}) | ||
if (!workspace) | ||
throw new TRPCError({ | ||
code: 'NOT_FOUND', | ||
message: 'Workspace not found', | ||
}) | ||
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY, { | ||
apiVersion: '2022-11-15', | ||
}) | ||
|
||
const session = await stripe.checkout.sessions.create({ | ||
success_url: `${returnUrl}?stripe=${plan}&success=true`, | ||
cancel_url: `${returnUrl}?stripe=cancel`, | ||
allow_promotion_codes: true, | ||
customer_email: prefilledEmail, | ||
mode: 'subscription', | ||
metadata: { workspaceId, plan, additionalChats, additionalStorage }, | ||
currency, | ||
automatic_tax: { enabled: true }, | ||
line_items: parseSubscriptionItems( | ||
plan, | ||
additionalChats, | ||
additionalStorage | ||
), | ||
}) | ||
|
||
if (!session.url) | ||
throw new TRPCError({ | ||
code: 'INTERNAL_SERVER_ERROR', | ||
message: 'Stripe checkout session creation failed', | ||
}) | ||
|
||
return { | ||
checkoutUrl: session.url, | ||
} | ||
} | ||
) |
58 changes: 58 additions & 0 deletions
58
apps/builder/src/features/billing/api/procedures/getBillingPortalUrl.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,58 @@ | ||
import prisma from '@/lib/prisma' | ||
import { authenticatedProcedure } from '@/utils/server/trpc' | ||
import { TRPCError } from '@trpc/server' | ||
import { WorkspaceRole } from 'db' | ||
import Stripe from 'stripe' | ||
import { z } from 'zod' | ||
|
||
export const getBillingPortalUrl = authenticatedProcedure | ||
.meta({ | ||
openapi: { | ||
method: 'GET', | ||
path: '/billing/subscription/portal', | ||
protect: true, | ||
summary: 'Get Stripe billing portal URL', | ||
tags: ['Billing'], | ||
}, | ||
}) | ||
.input( | ||
z.object({ | ||
workspaceId: z.string(), | ||
}) | ||
) | ||
.output( | ||
z.object({ | ||
billingPortalUrl: z.string(), | ||
}) | ||
) | ||
.query(async ({ input: { workspaceId }, ctx: { user } }) => { | ||
if (!process.env.STRIPE_SECRET_KEY) | ||
throw new TRPCError({ | ||
code: 'INTERNAL_SERVER_ERROR', | ||
message: 'STRIPE_SECRET_KEY var is missing', | ||
}) | ||
const workspace = await prisma.workspace.findFirst({ | ||
where: { | ||
id: workspaceId, | ||
members: { some: { userId: user.id, role: WorkspaceRole.ADMIN } }, | ||
}, | ||
select: { | ||
stripeId: true, | ||
}, | ||
}) | ||
if (!workspace?.stripeId) | ||
throw new TRPCError({ | ||
code: 'NOT_FOUND', | ||
message: 'Workspace not found', | ||
}) | ||
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY, { | ||
apiVersion: '2022-11-15', | ||
}) | ||
const portalSession = await stripe.billingPortal.sessions.create({ | ||
customer: workspace.stripeId, | ||
return_url: `${process.env.NEXTAUTH_URL}/typebots`, | ||
}) | ||
return { | ||
billingPortalUrl: portalSession.url, | ||
} | ||
}) |
81 changes: 81 additions & 0 deletions
81
apps/builder/src/features/billing/api/procedures/getSubscription.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,81 @@ | ||
import prisma from '@/lib/prisma' | ||
import { authenticatedProcedure } from '@/utils/server/trpc' | ||
import { TRPCError } from '@trpc/server' | ||
import { WorkspaceRole } from 'db' | ||
import Stripe from 'stripe' | ||
import { z } from 'zod' | ||
import { subscriptionSchema } from 'models/features/billing/subscription' | ||
|
||
export const getSubscription = authenticatedProcedure | ||
.meta({ | ||
openapi: { | ||
method: 'GET', | ||
path: '/billing/subscription', | ||
protect: true, | ||
summary: 'List invoices', | ||
tags: ['Billing'], | ||
}, | ||
}) | ||
.input( | ||
z.object({ | ||
workspaceId: z.string(), | ||
}) | ||
) | ||
.output( | ||
z.object({ | ||
subscription: subscriptionSchema, | ||
}) | ||
) | ||
.query(async ({ input: { workspaceId }, ctx: { user } }) => { | ||
if ( | ||
!process.env.STRIPE_SECRET_KEY || | ||
!process.env.STRIPE_ADDITIONAL_CHATS_PRICE_ID || | ||
!process.env.STRIPE_ADDITIONAL_STORAGE_PRICE_ID | ||
) | ||
throw new TRPCError({ | ||
code: 'INTERNAL_SERVER_ERROR', | ||
message: 'Stripe environment variables are missing', | ||
}) | ||
const workspace = await prisma.workspace.findFirst({ | ||
where: { | ||
id: workspaceId, | ||
members: { some: { userId: user.id, role: WorkspaceRole.ADMIN } }, | ||
}, | ||
}) | ||
if (!workspace?.stripeId) | ||
throw new TRPCError({ | ||
code: 'NOT_FOUND', | ||
message: 'Workspace not found', | ||
}) | ||
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY, { | ||
apiVersion: '2022-11-15', | ||
}) | ||
const subscriptions = await stripe.subscriptions.list({ | ||
customer: workspace.stripeId, | ||
limit: 1, | ||
}) | ||
|
||
const subscription = subscriptions?.data.shift() | ||
|
||
if (!subscription) | ||
throw new TRPCError({ | ||
code: 'NOT_FOUND', | ||
message: 'Subscription not found', | ||
}) | ||
|
||
return { | ||
subscription: { | ||
additionalChatsIndex: | ||
subscription?.items.data.find( | ||
(item) => | ||
item.price.id === process.env.STRIPE_ADDITIONAL_CHATS_PRICE_ID | ||
)?.quantity ?? 0, | ||
additionalStorageIndex: | ||
subscription.items.data.find( | ||
(item) => | ||
item.price.id === process.env.STRIPE_ADDITIONAL_STORAGE_PRICE_ID | ||
)?.quantity ?? 0, | ||
currency: subscription.currency as 'usd' | 'eur', | ||
}, | ||
} | ||
}) |
Oops, something went wrong.