wip: zen black

This commit is contained in:
Frank
2026-01-22 16:59:32 -05:00
parent fdac21688c
commit 5f3ab9395f
17 changed files with 1697 additions and 418 deletions

View File

@@ -0,0 +1 @@
ALTER TABLE `billing` ADD `time_subscription_selected` timestamp(3);

File diff suppressed because it is too large Load Diff

View File

@@ -386,6 +386,13 @@
"when": 1768603665356,
"tag": "0054_numerous_annihilus",
"breakpoints": true
},
{
"idx": 55,
"version": "5",
"when": 1769108945841,
"tag": "0055_moaning_karnak",
"breakpoints": true
}
]
}
}

View File

@@ -5,8 +5,11 @@ import { BillingTable, PaymentTable, SubscriptionTable } from "../src/schema/bil
import { Identifier } from "../src/identifier.js"
import { centsToMicroCents } from "../src/util/price.js"
import { AuthTable } from "../src/schema/auth.sql.js"
import { BlackData } from "../src/black.js"
import { Actor } from "../src/actor.js"
const plan = "200"
const couponID = "JAIr0Pe1"
const workspaceID = process.argv[2]
const seats = parseInt(process.argv[3])
@@ -61,16 +64,18 @@ const customerID =
.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`,
price: BlackData.planToPriceID({ plan }),
discounts: [{ coupon: couponID }],
quantity: seats,
},
],
metadata: {
workspaceID,
},
})
console.log(`Subscription ID: ${subscription.id}`)

View File

@@ -1,173 +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 foo.ts <workspaceID> <email>")
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)"}`)

View File

@@ -244,7 +244,7 @@ function getSubscriptionStatus(row: {
return { weekly: null, rolling: null, rateLimited: null, retryIn: null }
}
const black = BlackData.get({ plan: row.subscription.plan })
const black = BlackData.getLimits({ plan: row.subscription.plan })
const now = new Date()
const week = getWeekBounds(now)

View File

@@ -1,6 +1,6 @@
import { Stripe } from "stripe"
import { Database, eq, sql } from "./drizzle"
import { BillingTable, PaymentTable, UsageTable } from "./schema/billing.sql"
import { BillingTable, PaymentTable, SubscriptionTable, UsageTable } from "./schema/billing.sql"
import { Actor } from "./actor"
import { fn } from "./util/fn"
import { z } from "zod"
@@ -8,6 +8,7 @@ import { Resource } from "@opencode-ai/console-resource"
import { Identifier } from "./identifier"
import { centsToMicroCents } from "./util/price"
import { User } from "./user"
import { BlackData } from "./black"
export namespace Billing {
export const ITEM_CREDIT_NAME = "opencode credits"
@@ -288,4 +289,66 @@ export namespace Billing {
return charge.receipt_url
},
)
export const subscribe = fn(z.object({
seats: z.number(),
coupon: z.string().optional(),
}), async ({ seats, coupon }) => {
const user = Actor.assert("user")
const billing = await Database.use((tx) =>
tx
.select({
customerID: BillingTable.customerID,
paymentMethodID: BillingTable.paymentMethodID,
subscriptionID: BillingTable.subscriptionID,
subscriptionPlan: BillingTable.subscriptionPlan,
timeSubscriptionSelected: BillingTable.timeSubscriptionSelected,
})
.from(BillingTable)
.where(eq(BillingTable.workspaceID, Actor.workspace()))
.then((rows) => rows[0]),
)
if (!billing) throw new Error("Billing record not found")
if (!billing.timeSubscriptionSelected) throw new Error("Not selected for subscription")
if (billing.subscriptionID) throw new Error("Already subscribed")
if (!billing.customerID) throw new Error("No customer ID")
if (!billing.paymentMethodID) throw new Error("No payment method")
if (!billing.subscriptionPlan) throw new Error("No subscription plan")
const subscription = await Billing.stripe().subscriptions.create({
customer: billing.customerID,
default_payment_method: billing.paymentMethodID,
items: [{ price: BlackData.planToPriceID({ plan: billing.subscriptionPlan }) }],
metadata: {
workspaceID: Actor.workspace(),
},
})
await Database.transaction(async (tx) => {
await tx
.update(BillingTable)
.set({
subscriptionID: subscription.id,
subscription: {
status: "subscribed",
coupon,
seats,
plan: billing.subscriptionPlan!,
},
subscriptionPlan: null,
timeSubscriptionBooked: null,
timeSubscriptionSelected: null,
})
.where(eq(BillingTable.workspaceID, Actor.workspace()))
await tx.insert(SubscriptionTable).values({
workspaceID: Actor.workspace(),
id: Identifier.create("subscription"),
userID: user.properties.userID,
})
})
return subscription.id
})
}

View File

@@ -28,15 +28,28 @@ export namespace BlackData {
return input
})
export const get = fn(
z.object({
export const getLimits = fn(z.object({
plan: z.enum(SubscriptionPlan),
}),
({ plan }) => {
const json = JSON.parse(Resource.ZEN_BLACK_LIMITS.value)
return Schema.parse(json)[plan]
},
)
}), ({ plan }) => {
const json = JSON.parse(Resource.ZEN_BLACK_LIMITS.value)
return Schema.parse(json)[plan]
})
export const planToPriceID = fn(z.object({
plan: z.enum(SubscriptionPlan),
}), ({ plan }) => {
if (plan === "200") return Resource.ZEN_BLACK_PRICE.plan200
if (plan === "100") return Resource.ZEN_BLACK_PRICE.plan100
return Resource.ZEN_BLACK_PRICE.plan20
})
export const priceIDToPlan = fn(z.object({
priceID: z.string(),
}), ({ priceID }) => {
if (priceID === Resource.ZEN_BLACK_PRICE.plan200) return "200"
if (priceID === Resource.ZEN_BLACK_PRICE.plan100) return "100"
return "20"
})
}
export namespace Black {
@@ -48,7 +61,7 @@ export namespace Black {
}),
({ plan, usage, timeUpdated }) => {
const now = new Date()
const black = BlackData.get({ plan })
const black = BlackData.getLimits({ plan })
const rollingWindowMs = black.rollingWindow * 3600 * 1000
const rollingLimitInMicroCents = centsToMicroCents(black.rollingLimit * 100)
const windowStart = new Date(now.getTime() - rollingWindowMs)
@@ -83,7 +96,7 @@ export namespace Black {
timeUpdated: z.date(),
}),
({ plan, usage, timeUpdated }) => {
const black = BlackData.get({ plan })
const black = BlackData.getLimits({ plan })
const now = new Date()
const week = getWeekBounds(now)
const fixedLimitInMicroCents = centsToMicroCents(black.fixedLimit * 100)

View File

@@ -31,6 +31,7 @@ export const BillingTable = mysqlTable(
subscriptionID: varchar("subscription_id", { length: 28 }),
subscriptionPlan: mysqlEnum("subscription_plan", SubscriptionPlan),
timeSubscriptionBooked: utc("time_subscription_booked"),
timeSubscriptionSelected: utc("time_subscription_selected"),
},
(table) => [
...workspaceIndexes(table),