Openstatus
www.openstatus.dev
1import { TRPCError } from "@trpc/server";
2import type Stripe from "stripe";
3import { z } from "zod";
4
5import { Events, setupAnalytics } from "@openstatus/analytics";
6import { eq } from "@openstatus/db";
7import {
8 selectWorkspaceSchema,
9 user,
10 workspace,
11} from "@openstatus/db/src/schema";
12
13import {
14 getLimits,
15 updateAddonInLimits,
16} from "@openstatus/db/src/schema/plan/utils";
17import { createTRPCRouter, publicProcedure } from "../../trpc";
18import { stripe } from "./shared";
19import { getFeatureFromPriceId, getPlanFromPriceId } from "./utils";
20
21const webhookProcedure = publicProcedure.input(
22 z.object({
23 // From type Stripe.Event
24 event: z.object({
25 id: z.string(),
26 account: z.string().nullish(),
27 created: z.number(),
28 data: z.object({
29 object: z.record(z.string(), z.any()),
30 }),
31 type: z.string(),
32 }),
33 }),
34);
35
36export const webhookRouter = createTRPCRouter({
37 customerSubscriptionUpdated: webhookProcedure.mutation(async (opts) => {
38 const subscription = opts.input.event.data.object as Stripe.Subscription;
39
40 const customerId =
41 typeof subscription.customer === "string"
42 ? subscription.customer
43 : subscription.customer.id;
44
45 const result = await opts.ctx.db
46 .select()
47 .from(workspace)
48 .where(eq(workspace.stripeId, customerId))
49 .get();
50 if (!result) {
51 throw new TRPCError({
52 code: "BAD_REQUEST",
53 message: "Workspace not found",
54 });
55 }
56
57 // for (const item of subscription.items.data) {
58 // const feature = getFeatureFromPriceId(item.price.id);
59 // if (!feature) {
60 // continue;
61 // }
62 // const _ws = await opts.ctx.db
63 // .select()
64 // .from(workspace)
65 // .where(eq(workspace.stripeId, customerId))
66 // .get();
67
68 // const ws = selectWorkspaceSchema.parse(_ws);
69
70 // const currentValue = ws.limits[feature.feature];
71 // const newValue =
72 // typeof currentValue === "boolean"
73 // ? true
74 // : typeof currentValue === "number"
75 // ? currentValue + 1
76 // : currentValue;
77
78 // const newLimits = updateAddonInLimits(
79 // ws.limits,
80 // feature.feature,
81 // newValue,
82 // );
83
84 // await opts.ctx.db
85 // .update(workspace)
86 // .set({
87 // limits: JSON.stringify(newLimits),
88 // })
89 // .where(eq(workspace.id, result.id))
90 // .run();
91 // }
92
93 const customer = await stripe.customers.retrieve(customerId);
94 if (!customer.deleted && customer.email) {
95 const userResult = await opts.ctx.db
96 .select()
97 .from(user)
98 .where(eq(user.email, customer.email))
99 .get();
100 if (!userResult) return;
101 }
102 }),
103 sessionCompleted: webhookProcedure.mutation(async (opts) => {
104 const session = opts.input.event.data.object as Stripe.Checkout.Session;
105 if (typeof session.subscription !== "string") {
106 throw new TRPCError({
107 code: "BAD_REQUEST",
108 message: "Missing or invalid subscription id",
109 });
110 }
111 const subscription = await stripe.subscriptions.retrieve(
112 session.subscription,
113 );
114 const customerId =
115 typeof subscription.customer === "string"
116 ? subscription.customer
117 : subscription.customer.id;
118
119 const result = await opts.ctx.db
120 .select()
121 .from(workspace)
122 .where(eq(workspace.stripeId, customerId))
123 .get();
124 if (!result) {
125 throw new TRPCError({
126 code: "BAD_REQUEST",
127 message: "Workspace not found",
128 });
129 }
130
131 for (const item of subscription.items.data) {
132 const plan = getPlanFromPriceId(item.price.id);
133 if (!plan) {
134 const feature = getFeatureFromPriceId(item.price.id);
135 if (feature) {
136 const _ws = await opts.ctx.db
137 .select()
138 .from(workspace)
139 .where(eq(workspace.stripeId, customerId))
140 .get();
141
142 const ws = selectWorkspaceSchema.parse(_ws);
143
144 const currentValue = ws.limits[feature.feature];
145 const newValue =
146 typeof currentValue === "boolean"
147 ? true
148 : typeof currentValue === "number"
149 ? currentValue + 1
150 : currentValue;
151
152 const newLimits = updateAddonInLimits(
153 ws.limits,
154 feature.feature,
155 newValue,
156 );
157
158 await opts.ctx.db
159 .update(workspace)
160 .set({
161 limits: JSON.stringify(newLimits),
162 })
163 .where(eq(workspace.id, result.id))
164 .run();
165 continue;
166 }
167 console.error("Invalid plan");
168 throw new TRPCError({
169 code: "BAD_REQUEST",
170 message: "Invalid plan",
171 });
172 }
173 await opts.ctx.db
174 .update(workspace)
175 .set({
176 plan: plan.plan,
177 subscriptionId: subscription.id,
178 endsAt: new Date(subscription.current_period_end * 1000),
179 paidUntil: new Date(subscription.current_period_end * 1000),
180 limits: JSON.stringify(getLimits(plan.plan)),
181 })
182 .where(eq(workspace.id, result.id))
183 .run();
184 const customer = await stripe.customers.retrieve(customerId);
185 if (!customer.deleted && customer.email) {
186 const userResult = await opts.ctx.db
187 .select()
188 .from(user)
189 .where(eq(user.email, customer.email))
190 .get();
191 if (!userResult) return;
192
193 const analytics = await setupAnalytics({
194 userId: `usr_${userResult.id}`,
195 email: userResult.email || undefined,
196 workspaceId: String(result.id),
197 plan: plan.plan,
198 });
199 await analytics.track(Events.UpgradeWorkspace);
200 }
201 }
202 }),
203 customerSubscriptionDeleted: webhookProcedure.mutation(async (opts) => {
204 const subscription = opts.input.event.data.object as Stripe.Subscription;
205 const customerId =
206 typeof subscription.customer === "string"
207 ? subscription.customer
208 : subscription.customer.id;
209
210 const _workspace = await opts.ctx.db
211 .update(workspace)
212 .set({
213 subscriptionId: null,
214 plan: "free",
215 paidUntil: null,
216 })
217 .where(eq(workspace.stripeId, customerId))
218 .returning();
219
220 const customer = await stripe.customers.retrieve(customerId);
221
222 if (!_workspace) {
223 throw new TRPCError({
224 code: "BAD_REQUEST",
225 message: "Workspace not found",
226 });
227 }
228
229 if (!customer.deleted && customer.email) {
230 const userResult = await opts.ctx.db
231 .select()
232 .from(user)
233 .where(eq(user.email, customer.email))
234 .get();
235 if (!userResult) return;
236
237 const analytics = await setupAnalytics({
238 userId: `usr_${userResult.id}`,
239 email: customer.email || undefined,
240 workspaceId: String(_workspace[0].id),
241 plan: "free",
242 });
243 await analytics.track(Events.DowngradeWorkspace);
244 }
245 }),
246});