summaryrefslogtreecommitdiffhomepage
path: root/packages/console/core/src
diff options
context:
space:
mode:
authorFrank <[email protected]>2026-01-22 16:59:32 -0500
committerFrank <[email protected]>2026-01-22 17:02:46 -0500
commit5f3ab9395fc8f8543724dcda914d38fba0809049 (patch)
tree3be83aa5cc4e4556b62f9ff6b23933f8699eea63 /packages/console/core/src
parentfdac21688c9acc4087b83e71cb7e0fd1d2d57f00 (diff)
downloadopencode-5f3ab9395fc8f8543724dcda914d38fba0809049.tar.gz
opencode-5f3ab9395fc8f8543724dcda914d38fba0809049.zip
wip: zen black
Diffstat (limited to 'packages/console/core/src')
-rw-r--r--packages/console/core/src/billing.ts65
-rw-r--r--packages/console/core/src/black.ts33
-rw-r--r--packages/console/core/src/schema/billing.sql.ts1
3 files changed, 88 insertions, 11 deletions
diff --git a/packages/console/core/src/billing.ts b/packages/console/core/src/billing.ts
index 36e8a76b7..373f0c595 100644
--- a/packages/console/core/src/billing.ts
+++ b/packages/console/core/src/billing.ts
@@ -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
+ })
}
diff --git a/packages/console/core/src/black.ts b/packages/console/core/src/black.ts
index 13314a62d..93134de55 100644
--- a/packages/console/core/src/black.ts
+++ b/packages/console/core/src/black.ts
@@ -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)
diff --git a/packages/console/core/src/schema/billing.sql.ts b/packages/console/core/src/schema/billing.sql.ts
index ae32ed5ce..6f5c55850 100644
--- a/packages/console/core/src/schema/billing.sql.ts
+++ b/packages/console/core/src/schema/billing.sql.ts
@@ -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),