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

@@ -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),