@@ -2,6 +2,7 @@ import { Billing } from "@opencode-ai/console-core/billing.js"
22import type { APIEvent } from "@solidjs/start/server"
33import { and , Database , eq , sql } from "@opencode-ai/console-core/drizzle/index.js"
44import { BillingTable , PaymentTable } from "@opencode-ai/console-core/schema/billing.sql.js"
5+ import { UserTable } from "@opencode-ai/console-core/schema/user.sql.js"
56import { Identifier } from "@opencode-ai/console-core/identifier.js"
67import { centsToMicroCents } from "@opencode-ai/console-core/util/price.js"
78import { Actor } from "@opencode-ai/console-core/actor.js"
@@ -146,6 +147,249 @@ export async function POST(input: APIEvent) {
146147 . where ( eq ( BillingTable . workspaceID , workspaceID ) )
147148 } )
148149 }
150+ if ( body . type === "invoice.payment_succeeded" ) {
151+ const invoice = body . data . object
152+ if ( invoice . billing_reason === "subscription_cycle" ) {
153+ const invoiceID = invoice . id as string
154+ const amountInCents = invoice . amount_paid
155+ const customerID = invoice . customer as string
156+ const subscriptionID = invoice . parent ?. subscription_details ?. subscription as string
157+
158+ if ( ! customerID ) throw new Error ( "Customer ID not found" )
159+ if ( ! invoiceID ) throw new Error ( "Invoice ID not found" )
160+ if ( ! subscriptionID ) throw new Error ( "Subscription ID not found" )
161+
162+ const payment = await Billing . stripe ( ) . invoicePayments . retrieve ( invoiceID )
163+ const paymentID = payment . id as string
164+ if ( ! paymentID ) throw new Error ( "Payment ID not found" )
165+
166+ const workspaceID = await Database . use ( ( tx ) =>
167+ tx
168+ . select ( { workspaceID : BillingTable . workspaceID } )
169+ . from ( BillingTable )
170+ . where ( eq ( BillingTable . customerID , customerID ) )
171+ . then ( ( rows ) => rows [ 0 ] ?. workspaceID ) ,
172+ )
173+ if ( ! workspaceID ) throw new Error ( "Workspace ID not found for customer" )
174+
175+ await Database . transaction ( async ( tx ) => {
176+ await tx
177+ . update ( BillingTable )
178+ . set ( {
179+ balance : sql `${ BillingTable . balance } + ${ centsToMicroCents ( amountInCents ) } ` ,
180+ } )
181+ . where ( eq ( BillingTable . workspaceID , workspaceID ) )
182+ await tx . insert ( PaymentTable ) . values ( {
183+ workspaceID,
184+ id : Identifier . create ( "payment" ) ,
185+ amount : centsToMicroCents ( amountInCents ) ,
186+ paymentID,
187+ invoiceID,
188+ customerID,
189+ } )
190+ } )
191+ }
192+ }
193+ if ( body . type === "customer.subscription.created" ) {
194+ const data = {
195+ id : "evt_1Smq802SrMQ2Fneksse5FMNV" ,
196+ object : "event" ,
197+ api_version : "2025-07-30.basil" ,
198+ created : 1767766916 ,
199+ data : {
200+ object : {
201+ id : "sub_1Smq7x2SrMQ2Fnek8F1yf3ZD" ,
202+ object : "subscription" ,
203+ application : null ,
204+ application_fee_percent : null ,
205+ automatic_tax : {
206+ disabled_reason : null ,
207+ enabled : false ,
208+ liability : null ,
209+ } ,
210+ billing_cycle_anchor : 1770445200 ,
211+ billing_cycle_anchor_config : null ,
212+ billing_mode : {
213+ flexible : {
214+ proration_discounts : "included" ,
215+ } ,
216+ type : "flexible" ,
217+ updated_at : 1770445200 ,
218+ } ,
219+ billing_thresholds : null ,
220+ cancel_at : null ,
221+ cancel_at_period_end : false ,
222+ canceled_at : null ,
223+ cancellation_details : {
224+ comment : null ,
225+ feedback : null ,
226+ reason : null ,
227+ } ,
228+ collection_method : "charge_automatically" ,
229+ created : 1770445200 ,
230+ currency : "usd" ,
231+ customer : "cus_TkKmZZvysJ2wej" ,
232+ customer_account : null ,
233+ days_until_due : null ,
234+ default_payment_method : null ,
235+ default_source : "card_1Smq7u2SrMQ2FneknjyOa7sq" ,
236+ default_tax_rates : [ ] ,
237+ description : null ,
238+ discounts : [ ] ,
239+ ended_at : null ,
240+ invoice_settings : {
241+ account_tax_ids : null ,
242+ issuer : {
243+ type : "self" ,
244+ } ,
245+ } ,
246+ items : {
247+ object : "list" ,
248+ data : [
249+ {
250+ id : "si_TkKnBKXFX76t0O" ,
251+ object : "subscription_item" ,
252+ billing_thresholds : null ,
253+ created : 1770445200 ,
254+ current_period_end : 1772864400 ,
255+ current_period_start : 1770445200 ,
256+ discounts : [ ] ,
257+ metadata : { } ,
258+ plan : {
259+ id : "price_1SmfFG2SrMQ2FnekJuzwHMea" ,
260+ object : "plan" ,
261+ active : true ,
262+ amount : 20000 ,
263+ amount_decimal : "20000" ,
264+ billing_scheme : "per_unit" ,
265+ created : 1767725082 ,
266+ currency : "usd" ,
267+ interval : "month" ,
268+ interval_count : 1 ,
269+ livemode : false ,
270+ metadata : { } ,
271+ meter : null ,
272+ nickname : null ,
273+ product : "prod_Tk9LjWT1n0DgYm" ,
274+ tiers_mode : null ,
275+ transform_usage : null ,
276+ trial_period_days : null ,
277+ usage_type : "licensed" ,
278+ } ,
279+ price : {
280+ id : "price_1SmfFG2SrMQ2FnekJuzwHMea" ,
281+ object : "price" ,
282+ active : true ,
283+ billing_scheme : "per_unit" ,
284+ created : 1767725082 ,
285+ currency : "usd" ,
286+ custom_unit_amount : null ,
287+ livemode : false ,
288+ lookup_key : null ,
289+ metadata : { } ,
290+ nickname : null ,
291+ product : "prod_Tk9LjWT1n0DgYm" ,
292+ recurring : {
293+ interval : "month" ,
294+ interval_count : 1 ,
295+ meter : null ,
296+ trial_period_days : null ,
297+ usage_type : "licensed" ,
298+ } ,
299+ tax_behavior : "unspecified" ,
300+ tiers_mode : null ,
301+ transform_quantity : null ,
302+ type : "recurring" ,
303+ unit_amount : 20000 ,
304+ unit_amount_decimal : "20000" ,
305+ } ,
306+ quantity : 1 ,
307+ subscription : "sub_1Smq7x2SrMQ2Fnek8F1yf3ZD" ,
308+ tax_rates : [ ] ,
309+ } ,
310+ ] ,
311+ has_more : false ,
312+ total_count : 1 ,
313+ url : "/v1/subscription_items?subscription=sub_1Smq7x2SrMQ2Fnek8F1yf3ZD" ,
314+ } ,
315+ latest_invoice : "in_1Smq7x2SrMQ2FnekSJesfPwE" ,
316+ livemode : false ,
317+ metadata : { } ,
318+ next_pending_invoice_item_invoice : null ,
319+ on_behalf_of : null ,
320+ pause_collection : null ,
321+ payment_settings : {
322+ payment_method_options : null ,
323+ payment_method_types : null ,
324+ save_default_payment_method : "off" ,
325+ } ,
326+ pending_invoice_item_interval : null ,
327+ pending_setup_intent : null ,
328+ pending_update : null ,
329+ plan : {
330+ id : "price_1SmfFG2SrMQ2FnekJuzwHMea" ,
331+ object : "plan" ,
332+ active : true ,
333+ amount : 20000 ,
334+ amount_decimal : "20000" ,
335+ billing_scheme : "per_unit" ,
336+ created : 1767725082 ,
337+ currency : "usd" ,
338+ interval : "month" ,
339+ interval_count : 1 ,
340+ livemode : false ,
341+ metadata : { } ,
342+ meter : null ,
343+ nickname : null ,
344+ product : "prod_Tk9LjWT1n0DgYm" ,
345+ tiers_mode : null ,
346+ transform_usage : null ,
347+ trial_period_days : null ,
348+ usage_type : "licensed" ,
349+ } ,
350+ quantity : 1 ,
351+ schedule : null ,
352+ start_date : 1770445200 ,
353+ status : "active" ,
354+ test_clock : "clock_1Smq6n2SrMQ2FnekQw4yt2PZ" ,
355+ transfer_data : null ,
356+ trial_end : null ,
357+ trial_settings : {
358+ end_behavior : {
359+ missing_payment_method : "create_invoice" ,
360+ } ,
361+ } ,
362+ trial_start : null ,
363+ } ,
364+ } ,
365+ livemode : false ,
366+ pending_webhooks : 0 ,
367+ request : {
368+ id : "req_6YO9stvB155WJD" ,
369+ idempotency_key : "581ba059-6f86-49b2-9c49-0d8450255322" ,
370+ } ,
371+ type : "customer.subscription.created" ,
372+ }
373+ }
374+ if ( body . type === "customer.subscription.deleted" ) {
375+ const subscriptionID = body . data . object . id
376+ if ( ! subscriptionID ) throw new Error ( "Subscription ID not found" )
377+
378+ const workspaceID = await Database . use ( ( tx ) =>
379+ tx
380+ . select ( { workspaceID : BillingTable . workspaceID } )
381+ . from ( BillingTable )
382+ . where ( eq ( BillingTable . subscriptionID , subscriptionID ) )
383+ . then ( ( rows ) => rows [ 0 ] ?. workspaceID ) ,
384+ )
385+ if ( ! workspaceID ) throw new Error ( "Workspace ID not found for subscription" )
386+
387+ await Database . transaction ( async ( tx ) => {
388+ await tx . update ( BillingTable ) . set ( { subscriptionID : null } ) . where ( eq ( BillingTable . workspaceID , workspaceID ) )
389+
390+ await tx . update ( UserTable ) . set ( { timeSubscribed : null } ) . where ( eq ( UserTable . workspaceID , workspaceID ) )
391+ } )
392+ }
149393 } ) ( )
150394 . then ( ( message ) => {
151395 return Response . json ( { message : message ?? "done" } , { status : 200 } )
0 commit comments