Openstatus www.openstatus.dev
at 4c0f4c00a38753a5d0dfd7e7b7b7706dec6f1503 246 lines 7.1 kB view raw
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});