Buckets:
| import { Billing } from "../src/billing.js" | |
| import { and, Database, desc, eq, isNotNull, lt, sql } from "../src/drizzle/index.js" | |
| import { BillingTable, PaymentTable, SubscriptionTable } from "../src/schema/billing.sql.js" | |
| const fromWrkID = process.argv[2] | |
| const toWrkID = process.argv[3] | |
| if (!fromWrkID || !toWrkID) { | |
| console.error("Usage: bun foo.ts <fromWrkID> <toWrkID>") | |
| process.exit(1) | |
| } | |
| console.log(`Transferring subscription from ${fromWrkID} to ${toWrkID}`) | |
| // Look up the FROM workspace billing | |
| const fromBilling = await Database.use((tx) => | |
| tx | |
| .select({ | |
| customerID: BillingTable.customerID, | |
| subscriptionID: BillingTable.subscriptionID, | |
| subscription: BillingTable.subscription, | |
| paymentMethodID: BillingTable.paymentMethodID, | |
| paymentMethodType: BillingTable.paymentMethodType, | |
| paymentMethodLast4: BillingTable.paymentMethodLast4, | |
| }) | |
| .from(BillingTable) | |
| .where(eq(BillingTable.workspaceID, fromWrkID)) | |
| .then((rows) => rows[0]), | |
| ) | |
| if (!fromBilling) throw new Error(`Error: FROM workspace has no billing record`) | |
| if (!fromBilling.customerID) throw new Error(`Error: FROM workspace has no Stripe customer ID`) | |
| if (!fromBilling.subscriptionID) throw new Error(`Error: FROM workspace has no subscription`) | |
| const fromSubscription = await Database.use((tx) => | |
| tx | |
| .select({ userID: SubscriptionTable.userID }) | |
| .from(SubscriptionTable) | |
| .where(eq(SubscriptionTable.workspaceID, fromWrkID)) | |
| .then((rows) => rows[0]), | |
| ) | |
| if (!fromSubscription) throw new Error(`Error: FROM workspace has no subscription`) | |
| // Look up the previous customer ID in FROM workspace | |
| const subscriptionPayment = await Database.use((tx) => | |
| tx | |
| .select({ | |
| customerID: PaymentTable.customerID, | |
| timeCreated: PaymentTable.timeCreated, | |
| }) | |
| .from(PaymentTable) | |
| .where(and(eq(PaymentTable.workspaceID, fromWrkID), sql`JSON_EXTRACT(enrichment, '$.type') = 'subscription'`)) | |
| .then((rows) => { | |
| if (rows.length > 1) { | |
| console.error(`Error: Multiple subscription payments found for workspace ${fromWrkID}`) | |
| process.exit(1) | |
| } | |
| return rows[0] | |
| }), | |
| ) | |
| const fromPrevPayment = await Database.use((tx) => | |
| tx | |
| .select({ customerID: PaymentTable.customerID }) | |
| .from(PaymentTable) | |
| .where( | |
| and( | |
| eq(PaymentTable.workspaceID, fromWrkID), | |
| isNotNull(PaymentTable.customerID), | |
| lt(PaymentTable.timeCreated, subscriptionPayment.timeCreated), | |
| ), | |
| ) | |
| .orderBy(desc(PaymentTable.timeCreated)) | |
| .limit(1) | |
| .then((rows) => rows[0]), | |
| ) | |
| if (!fromPrevPayment?.customerID) throw new Error(`Error: FROM workspace has no previous Stripe customer to revert to`) | |
| if (fromPrevPayment.customerID === fromBilling.customerID) | |
| throw new Error(`Error: FROM workspace has the same Stripe customer ID as the current one`) | |
| const fromPrevPaymentMethods = await Billing.stripe().customers.listPaymentMethods(fromPrevPayment.customerID, {}) | |
| if (fromPrevPaymentMethods.data.length === 0) | |
| throw new Error(`Error: FROM workspace has no previous Stripe payment methods`) | |
| // Look up the TO workspace billing | |
| const toBilling = await Database.use((tx) => | |
| tx | |
| .select({ | |
| customerID: BillingTable.customerID, | |
| subscriptionID: BillingTable.subscriptionID, | |
| }) | |
| .from(BillingTable) | |
| .where(eq(BillingTable.workspaceID, toWrkID)) | |
| .then((rows) => rows[0]), | |
| ) | |
| if (!toBilling) throw new Error(`Error: TO workspace has no billing record`) | |
| if (toBilling.subscriptionID) throw new Error(`Error: TO workspace already has a subscription`) | |
| console.log(`FROM:`) | |
| console.log(` Old Customer ID: ${fromBilling.customerID}`) | |
| console.log(` New Customer ID: ${fromPrevPayment.customerID}`) | |
| console.log(`TO:`) | |
| console.log(` Old Customer ID: ${toBilling.customerID}`) | |
| console.log(` New Customer ID: ${fromBilling.customerID}`) | |
| // Clear workspaceID from Stripe customer metadata | |
| await Billing.stripe().customers.update(fromPrevPayment.customerID, { | |
| metadata: { | |
| workspaceID: fromWrkID, | |
| }, | |
| }) | |
| await Billing.stripe().customers.update(fromBilling.customerID, { | |
| metadata: { | |
| workspaceID: toWrkID, | |
| }, | |
| }) | |
| await Database.transaction(async (tx) => { | |
| await tx | |
| .update(BillingTable) | |
| .set({ | |
| customerID: fromPrevPayment.customerID, | |
| subscriptionID: null, | |
| subscription: null, | |
| paymentMethodID: fromPrevPaymentMethods.data[0].id, | |
| paymentMethodLast4: fromPrevPaymentMethods.data[0].card?.last4 ?? null, | |
| paymentMethodType: fromPrevPaymentMethods.data[0].type, | |
| }) | |
| .where(eq(BillingTable.workspaceID, fromWrkID)) | |
| await tx | |
| .update(BillingTable) | |
| .set({ | |
| customerID: fromBilling.customerID, | |
| subscriptionID: fromBilling.subscriptionID, | |
| subscription: fromBilling.subscription, | |
| paymentMethodID: fromBilling.paymentMethodID, | |
| paymentMethodLast4: fromBilling.paymentMethodLast4, | |
| paymentMethodType: fromBilling.paymentMethodType, | |
| }) | |
| .where(eq(BillingTable.workspaceID, toWrkID)) | |
| await tx | |
| .update(SubscriptionTable) | |
| .set({ | |
| workspaceID: toWrkID, | |
| userID: fromSubscription.userID, | |
| }) | |
| .where(eq(SubscriptionTable.workspaceID, fromWrkID)) | |
| await tx | |
| .update(PaymentTable) | |
| .set({ | |
| workspaceID: toWrkID, | |
| }) | |
| .where( | |
| and( | |
| eq(PaymentTable.workspaceID, fromWrkID), | |
| sql`JSON_EXTRACT(enrichment, '$.type') = 'subscription'`, | |
| eq(PaymentTable.amount, 20000000000), | |
| ), | |
| ) | |
| }) | |
| console.log(`done`) | |
Xet Storage Details
- Size:
- 5.5 kB
- Xet hash:
- 5fda9a8faf2e1c839f74981912ec184ff5876f77abb898b840341f589ea06af3
·
Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.