From cbe1c81470564a8f5042047d7422219426e78030 Mon Sep 17 00:00:00 2001 From: Frank Date: Fri, 16 Jan 2026 17:46:32 -0500 Subject: wip: black --- packages/console/core/script/black-gift.ts | 112 ++++++++++++++ packages/console/core/script/black-onboard.ts | 173 ++++++++++++++++++++++ packages/console/core/script/black-transfer.ts | 6 +- packages/console/core/script/lookup-user.ts | 18 ++- packages/console/core/script/onboard-zen-black.ts | 168 --------------------- 5 files changed, 300 insertions(+), 177 deletions(-) create mode 100644 packages/console/core/script/black-gift.ts create mode 100644 packages/console/core/script/black-onboard.ts delete mode 100644 packages/console/core/script/onboard-zen-black.ts (limited to 'packages/console/core/script') diff --git a/packages/console/core/script/black-gift.ts b/packages/console/core/script/black-gift.ts new file mode 100644 index 000000000..3fbf210ab --- /dev/null +++ b/packages/console/core/script/black-gift.ts @@ -0,0 +1,112 @@ +import { Billing } from "../src/billing.js" +import { and, Database, eq, isNull, sql } from "../src/drizzle/index.js" +import { UserTable } from "../src/schema/user.sql.js" +import { BillingTable, PaymentTable, SubscriptionTable } from "../src/schema/billing.sql.js" +import { Identifier } from "../src/identifier.js" +import { centsToMicroCents } from "../src/util/price.js" +import { AuthTable } from "../src/schema/auth.sql.js" + +const plan = "200" +const workspaceID = process.argv[2] +const seats = parseInt(process.argv[3]) + +console.log(`Gifting ${seats} seats of Black to workspace ${workspaceID}`) + +if (!workspaceID || !seats) throw new Error("Usage: bun foo.ts ") + +// Get workspace user +const users = await Database.use((tx) => + tx + .select({ + id: UserTable.id, + role: UserTable.role, + email: AuthTable.subject, + }) + .from(UserTable) + .leftJoin(AuthTable, and(eq(AuthTable.accountID, UserTable.accountID), eq(AuthTable.provider, "email"))) + .where(and(eq(UserTable.workspaceID, workspaceID), isNull(UserTable.timeDeleted))), +) +if (users.length === 0) throw new Error(`Error: No users found in workspace ${workspaceID}`) +if (users.length !== seats) + throw new Error(`Error: Workspace ${workspaceID} has ${users.length} users, expected ${seats}`) +const adminUser = users.find((user) => user.role === "admin") +if (!adminUser) throw new Error(`Error: No admin user found in workspace ${workspaceID}`) +if (!adminUser.email) throw new Error(`Error: Admin user ${adminUser.id} has no email`) + +// Get Billing +const billing = await Database.use((tx) => + tx + .select({ + customerID: BillingTable.customerID, + subscriptionID: BillingTable.subscriptionID, + }) + .from(BillingTable) + .where(eq(BillingTable.workspaceID, workspaceID)) + .then((rows) => rows[0]), +) +if (!billing) throw new Error(`Error: Workspace ${workspaceID} has no billing record`) +if (billing.subscriptionID) throw new Error(`Error: Workspace ${workspaceID} already has a subscription`) + +// Look up the Stripe customer by email +const customerID = + billing.customerID ?? + (await (() => + Billing.stripe() + .customers.create({ + email: adminUser.email, + metadata: { + workspaceID, + }, + }) + .then((customer) => customer.id))()) +console.log(`Customer ID: ${customerID}`) + +const couponID = "JAIr0Pe1" +const subscription = await Billing.stripe().subscriptions.create({ + customer: customerID!, + items: [ + { + price: `price_1SmfyI2StuRr0lbXovxJNeZn`, + discounts: [{ coupon: couponID }], + quantity: 2, + }, + ], +}) +console.log(`Subscription ID: ${subscription.id}`) + +await Database.transaction(async (tx) => { + // Set customer id, subscription id, and payment method on workspace billing + await tx + .update(BillingTable) + .set({ + customerID, + subscriptionID: subscription.id, + subscription: { status: "subscribed", coupon: couponID, seats, plan }, + }) + .where(eq(BillingTable.workspaceID, workspaceID)) + + // Create a row in subscription table + for (const user of users) { + await tx.insert(SubscriptionTable).values({ + workspaceID, + id: Identifier.create("subscription"), + userID: user.id, + }) + } + // + // // Create a row in payments table + // await tx.insert(PaymentTable).values({ + // workspaceID, + // id: Identifier.create("payment"), + // amount: centsToMicroCents(amountInCents), + // customerID, + // invoiceID, + // paymentID, + // enrichment: { + // type: "subscription", + // couponID, + // }, + // }) +}) + +console.log(`done`) diff --git a/packages/console/core/script/black-onboard.ts b/packages/console/core/script/black-onboard.ts new file mode 100644 index 000000000..77e5b779e --- /dev/null +++ b/packages/console/core/script/black-onboard.ts @@ -0,0 +1,173 @@ +import { Billing } from "../src/billing.js" +import { and, Database, eq, isNull, sql } from "../src/drizzle/index.js" +import { UserTable } from "../src/schema/user.sql.js" +import { BillingTable, PaymentTable, SubscriptionTable } from "../src/schema/billing.sql.js" +import { Identifier } from "../src/identifier.js" +import { centsToMicroCents } from "../src/util/price.js" +import { AuthTable } from "../src/schema/auth.sql.js" + +const workspaceID = process.argv[2] +const email = process.argv[3] + +console.log(`Onboarding workspace ${workspaceID} for email ${email}`) + +if (!workspaceID || !email) { + console.error("Usage: bun foo.ts ") + process.exit(1) +} + +// Look up the Stripe customer by email +const customers = await Billing.stripe().customers.list({ email, limit: 10, expand: ["data.subscriptions"] }) +if (!customers.data) { + console.error(`Error: No Stripe customer found for email ${email}`) + process.exit(1) +} +const customer = customers.data.find((c) => c.subscriptions?.data[0]?.items.data[0]?.price.unit_amount === 20000) +if (!customer) { + console.error(`Error: No Stripe customer found for email ${email} with $200 subscription`) + process.exit(1) +} + +const customerID = customer.id +const subscription = customer.subscriptions!.data[0] +const subscriptionID = subscription.id + +// Validate the subscription is $200 +const amountInCents = subscription.items.data[0]?.price.unit_amount ?? 0 +if (amountInCents !== 20000) { + console.error(`Error: Subscription amount is $${amountInCents / 100}, expected $200`) + process.exit(1) +} + +const subscriptionData = await Billing.stripe().subscriptions.retrieve(subscription.id, { expand: ["discounts"] }) +const couponID = + typeof subscriptionData.discounts[0] === "string" + ? subscriptionData.discounts[0] + : subscriptionData.discounts[0]?.coupon?.id + +// Check if subscription is already tied to another workspace +const existingSubscription = await Database.use((tx) => + tx + .select({ workspaceID: BillingTable.workspaceID }) + .from(BillingTable) + .where(sql`JSON_EXTRACT(${BillingTable.subscription}, '$.id') = ${subscriptionID}`) + .then((rows) => rows[0]), +) +if (existingSubscription) { + console.error( + `Error: Subscription ${subscriptionID} is already tied to workspace ${existingSubscription.workspaceID}`, + ) + process.exit(1) +} + +// Look up the workspace billing and check if it already has a customer id or subscription +const billing = await Database.use((tx) => + tx + .select({ customerID: BillingTable.customerID, subscriptionID: BillingTable.subscriptionID }) + .from(BillingTable) + .where(eq(BillingTable.workspaceID, workspaceID)) + .then((rows) => rows[0]), +) +if (billing?.subscriptionID) { + console.error(`Error: Workspace ${workspaceID} already has a subscription: ${billing.subscriptionID}`) + process.exit(1) +} +if (billing?.customerID) { + console.warn( + `Warning: Workspace ${workspaceID} already has a customer id: ${billing.customerID}, replacing with ${customerID}`, + ) +} + +// Get the latest invoice and payment from the subscription +const invoices = await Billing.stripe().invoices.list({ + subscription: subscriptionID, + limit: 1, + expand: ["data.payments"], +}) +const invoice = invoices.data[0] +const invoiceID = invoice?.id +const paymentID = invoice?.payments?.data[0]?.payment.payment_intent as string | undefined + +// Get the default payment method from the customer +const paymentMethodID = (customer.invoice_settings.default_payment_method ?? subscription.default_payment_method) as + | string + | null +const paymentMethod = paymentMethodID ? await Billing.stripe().paymentMethods.retrieve(paymentMethodID) : null +const paymentMethodLast4 = paymentMethod?.card?.last4 ?? null +const paymentMethodType = paymentMethod?.type ?? null + +// Look up the user in the workspace +const users = await Database.use((tx) => + tx + .select({ id: UserTable.id, email: AuthTable.subject }) + .from(UserTable) + .innerJoin(AuthTable, and(eq(AuthTable.accountID, UserTable.accountID), eq(AuthTable.provider, "email"))) + .where(and(eq(UserTable.workspaceID, workspaceID), isNull(UserTable.timeDeleted))), +) +if (users.length === 0) { + console.error(`Error: No users found in workspace ${workspaceID}`) + process.exit(1) +} +const user = users.length === 1 ? users[0] : users.find((u) => u.email === email) +if (!user) { + console.error(`Error: User with email ${email} not found in workspace ${workspaceID}`) + process.exit(1) +} + +// Set workspaceID in Stripe customer metadata +await Billing.stripe().customers.update(customerID, { + metadata: { + workspaceID, + }, +}) + +await Database.transaction(async (tx) => { + // Set customer id, subscription id, and payment method on workspace billing + await tx + .update(BillingTable) + .set({ + customerID, + subscriptionID, + paymentMethodID, + paymentMethodLast4, + paymentMethodType, + subscription: { + status: "subscribed", + coupon: couponID, + seats: 1, + plan: "200", + }, + }) + .where(eq(BillingTable.workspaceID, workspaceID)) + + // Create a row in subscription table + await tx.insert(SubscriptionTable).values({ + workspaceID, + id: Identifier.create("subscription"), + userID: user.id, + }) + + // Create a row in payments table + await tx.insert(PaymentTable).values({ + workspaceID, + id: Identifier.create("payment"), + amount: centsToMicroCents(amountInCents), + customerID, + invoiceID, + paymentID, + enrichment: { + type: "subscription", + couponID, + }, + }) +}) + +console.log(`Successfully onboarded workspace ${workspaceID}`) +console.log(` Customer ID: ${customerID}`) +console.log(` Subscription ID: ${subscriptionID}`) +console.log( + ` Payment Method: ${paymentMethodID ?? "(none)"} (${paymentMethodType ?? "unknown"} ending in ${paymentMethodLast4 ?? "????"})`, +) +console.log(` User ID: ${user.id}`) +console.log(` Invoice ID: ${invoiceID ?? "(none)"}`) +console.log(` Payment ID: ${paymentID ?? "(none)"}`) diff --git a/packages/console/core/script/black-transfer.ts b/packages/console/core/script/black-transfer.ts index a7947fe72..e962ba5d3 100644 --- a/packages/console/core/script/black-transfer.ts +++ b/packages/console/core/script/black-transfer.ts @@ -18,7 +18,7 @@ const fromBilling = await Database.use((tx) => .select({ customerID: BillingTable.customerID, subscriptionID: BillingTable.subscriptionID, - subscriptionCouponID: BillingTable.subscriptionCouponID, + subscription: BillingTable.subscription, paymentMethodID: BillingTable.paymentMethodID, paymentMethodType: BillingTable.paymentMethodType, paymentMethodLast4: BillingTable.paymentMethodLast4, @@ -119,7 +119,7 @@ await Database.transaction(async (tx) => { .set({ customerID: fromPrevPayment.customerID, subscriptionID: null, - subscriptionCouponID: null, + subscription: null, paymentMethodID: fromPrevPaymentMethods.data[0].id, paymentMethodLast4: fromPrevPaymentMethods.data[0].card?.last4 ?? null, paymentMethodType: fromPrevPaymentMethods.data[0].type, @@ -131,7 +131,7 @@ await Database.transaction(async (tx) => { .set({ customerID: fromBilling.customerID, subscriptionID: fromBilling.subscriptionID, - subscriptionCouponID: fromBilling.subscriptionCouponID, + subscription: fromBilling.subscription, paymentMethodID: fromBilling.paymentMethodID, paymentMethodLast4: fromBilling.paymentMethodLast4, paymentMethodType: fromBilling.paymentMethodType, diff --git a/packages/console/core/script/lookup-user.ts b/packages/console/core/script/lookup-user.ts index b3a104457..3dc5e7a96 100644 --- a/packages/console/core/script/lookup-user.ts +++ b/packages/console/core/script/lookup-user.ts @@ -55,8 +55,9 @@ if (identifier.startsWith("wrk_")) { ), ) - // Get all payments for these workspaces - await Promise.all(users.map((u: { workspaceID: string }) => printWorkspace(u.workspaceID))) + for (const user of users) { + await printWorkspace(user.workspaceID) + } } async function printWorkspace(workspaceID: string) { @@ -114,11 +115,11 @@ async function printWorkspace(workspaceID: string) { balance: BillingTable.balance, customerID: BillingTable.customerID, reload: BillingTable.reload, + subscriptionID: BillingTable.subscriptionID, subscription: { - id: BillingTable.subscriptionID, - couponID: BillingTable.subscriptionCouponID, plan: BillingTable.subscriptionPlan, booked: BillingTable.timeSubscriptionBooked, + enrichment: BillingTable.subscription, }, }) .from(BillingTable) @@ -128,8 +129,13 @@ async function printWorkspace(workspaceID: string) { rows.map((row) => ({ ...row, balance: `$${(row.balance / 100000000).toFixed(2)}`, - subscription: row.subscription.id - ? `Subscribed ${row.subscription.couponID ? `(coupon: ${row.subscription.couponID}) ` : ""}` + subscription: row.subscriptionID + ? [ + `Black ${row.subscription.enrichment!.plan}`, + row.subscription.enrichment!.seats > 1 ? `X ${row.subscription.enrichment!.seats} seats` : "", + row.subscription.enrichment!.coupon ? `(coupon: ${row.subscription.enrichment!.coupon})` : "", + `(ref: ${row.subscriptionID})`, + ].join(" ") : row.subscription.booked ? `Waitlist ${row.subscription.plan} plan` : undefined, diff --git a/packages/console/core/script/onboard-zen-black.ts b/packages/console/core/script/onboard-zen-black.ts deleted file mode 100644 index 3ee880973..000000000 --- a/packages/console/core/script/onboard-zen-black.ts +++ /dev/null @@ -1,168 +0,0 @@ -import { Billing } from "../src/billing.js" -import { and, Database, eq, isNull, sql } from "../src/drizzle/index.js" -import { UserTable } from "../src/schema/user.sql.js" -import { BillingTable, PaymentTable, SubscriptionTable } from "../src/schema/billing.sql.js" -import { Identifier } from "../src/identifier.js" -import { centsToMicroCents } from "../src/util/price.js" -import { AuthTable } from "../src/schema/auth.sql.js" - -const workspaceID = process.argv[2] -const email = process.argv[3] - -console.log(`Onboarding workspace ${workspaceID} for email ${email}`) - -if (!workspaceID || !email) { - console.error("Usage: bun onboard-zen-black.ts ") - process.exit(1) -} - -// Look up the Stripe customer by email -const customers = await Billing.stripe().customers.list({ email, limit: 10, expand: ["data.subscriptions"] }) -if (!customers.data) { - console.error(`Error: No Stripe customer found for email ${email}`) - process.exit(1) -} -const customer = customers.data.find((c) => c.subscriptions?.data[0]?.items.data[0]?.price.unit_amount === 20000) -if (!customer) { - console.error(`Error: No Stripe customer found for email ${email} with $200 subscription`) - process.exit(1) -} - -const customerID = customer.id -const subscription = customer.subscriptions!.data[0] -const subscriptionID = subscription.id - -// Validate the subscription is $200 -const amountInCents = subscription.items.data[0]?.price.unit_amount ?? 0 -if (amountInCents !== 20000) { - console.error(`Error: Subscription amount is $${amountInCents / 100}, expected $200`) - process.exit(1) -} - -const subscriptionData = await Billing.stripe().subscriptions.retrieve(subscription.id, { expand: ["discounts"] }) -const couponID = - typeof subscriptionData.discounts[0] === "string" - ? subscriptionData.discounts[0] - : subscriptionData.discounts[0]?.coupon?.id - -// Check if subscription is already tied to another workspace -const existingSubscription = await Database.use((tx) => - tx - .select({ workspaceID: BillingTable.workspaceID }) - .from(BillingTable) - .where(eq(BillingTable.subscriptionID, subscriptionID)) - .then((rows) => rows[0]), -) -if (existingSubscription) { - console.error( - `Error: Subscription ${subscriptionID} is already tied to workspace ${existingSubscription.workspaceID}`, - ) - process.exit(1) -} - -// Look up the workspace billing and check if it already has a customer id or subscription -const billing = await Database.use((tx) => - tx - .select({ customerID: BillingTable.customerID, subscriptionID: BillingTable.subscriptionID }) - .from(BillingTable) - .where(eq(BillingTable.workspaceID, workspaceID)) - .then((rows) => rows[0]), -) -if (billing?.subscriptionID) { - console.error(`Error: Workspace ${workspaceID} already has a subscription: ${billing.subscriptionID}`) - process.exit(1) -} -if (billing?.customerID) { - console.warn( - `Warning: Workspace ${workspaceID} already has a customer id: ${billing.customerID}, replacing with ${customerID}`, - ) -} - -// Get the latest invoice and payment from the subscription -const invoices = await Billing.stripe().invoices.list({ - subscription: subscriptionID, - limit: 1, - expand: ["data.payments"], -}) -const invoice = invoices.data[0] -const invoiceID = invoice?.id -const paymentID = invoice?.payments?.data[0]?.payment.payment_intent as string | undefined - -// Get the default payment method from the customer -const paymentMethodID = (customer.invoice_settings.default_payment_method ?? subscription.default_payment_method) as - | string - | null -const paymentMethod = paymentMethodID ? await Billing.stripe().paymentMethods.retrieve(paymentMethodID) : null -const paymentMethodLast4 = paymentMethod?.card?.last4 ?? null -const paymentMethodType = paymentMethod?.type ?? null - -// Look up the user in the workspace -const users = await Database.use((tx) => - tx - .select({ id: UserTable.id, email: AuthTable.subject }) - .from(UserTable) - .innerJoin(AuthTable, and(eq(AuthTable.accountID, UserTable.accountID), eq(AuthTable.provider, "email"))) - .where(and(eq(UserTable.workspaceID, workspaceID), isNull(UserTable.timeDeleted))), -) -if (users.length === 0) { - console.error(`Error: No users found in workspace ${workspaceID}`) - process.exit(1) -} -const user = users.length === 1 ? users[0] : users.find((u) => u.email === email) -if (!user) { - console.error(`Error: User with email ${email} not found in workspace ${workspaceID}`) - process.exit(1) -} - -// Set workspaceID in Stripe customer metadata -await Billing.stripe().customers.update(customerID, { - metadata: { - workspaceID, - }, -}) - -await Database.transaction(async (tx) => { - // Set customer id, subscription id, and payment method on workspace billing - await tx - .update(BillingTable) - .set({ - customerID, - subscriptionID, - subscriptionCouponID: couponID, - paymentMethodID, - paymentMethodLast4, - paymentMethodType, - }) - .where(eq(BillingTable.workspaceID, workspaceID)) - - // Create a row in subscription table - await tx.insert(SubscriptionTable).values({ - workspaceID, - id: Identifier.create("subscription"), - userID: user.id, - }) - - // Create a row in payments table - await tx.insert(PaymentTable).values({ - workspaceID, - id: Identifier.create("payment"), - amount: centsToMicroCents(amountInCents), - customerID, - invoiceID, - paymentID, - enrichment: { - type: "subscription", - couponID, - }, - }) -}) - -console.log(`Successfully onboarded workspace ${workspaceID}`) -console.log(` Customer ID: ${customerID}`) -console.log(` Subscription ID: ${subscriptionID}`) -console.log( - ` Payment Method: ${paymentMethodID ?? "(none)"} (${paymentMethodType ?? "unknown"} ending in ${paymentMethodLast4 ?? "????"})`, -) -console.log(` User ID: ${user.id}`) -console.log(` Invoice ID: ${invoiceID ?? "(none)"}`) -console.log(` Payment ID: ${paymentID ?? "(none)"}`) -- cgit v1.2.3