diff --git a/packages/console/app/src/routes/stripe/webhook.ts b/packages/console/app/src/routes/stripe/webhook.ts
index c7f752327..fc7b18fbc 100644
--- a/packages/console/app/src/routes/stripe/webhook.ts
+++ b/packages/console/app/src/routes/stripe/webhook.ts
@@ -216,141 +216,71 @@ export async function POST(input: APIEvent) {
})
}
if (body.type === "customer.subscription.created") {
- const data = {
- id: "evt_1Smq802SrMQ2Fneksse5FMNV",
- object: "event",
- api_version: "2025-07-30.basil",
- created: 1767766916,
- data: {
- object: {
- id: "sub_1Smq7x2SrMQ2Fnek8F1yf3ZD",
- object: "subscription",
- application: null,
- application_fee_percent: null,
- automatic_tax: {
- disabled_reason: null,
- enabled: false,
- liability: null,
- },
- billing_cycle_anchor: 1770445200,
- billing_cycle_anchor_config: null,
- billing_mode: {
- flexible: {
- proration_discounts: "included",
- },
- type: "flexible",
- updated_at: 1770445200,
- },
+ /*
+{
+ id: "evt_1Smq802SrMQ2Fneksse5FMNV",
+ object: "event",
+ api_version: "2025-07-30.basil",
+ created: 1767766916,
+ data: {
+ object: {
+ id: "sub_1Smq7x2SrMQ2Fnek8F1yf3ZD",
+ object: "subscription",
+ application: null,
+ application_fee_percent: null,
+ automatic_tax: {
+ disabled_reason: null,
+ enabled: false,
+ liability: null,
+ },
+ billing_cycle_anchor: 1770445200,
+ billing_cycle_anchor_config: null,
+ billing_mode: {
+ flexible: {
+ proration_discounts: "included",
+ },
+ type: "flexible",
+ updated_at: 1770445200,
+ },
+ billing_thresholds: null,
+ cancel_at: null,
+ cancel_at_period_end: false,
+ canceled_at: null,
+ cancellation_details: {
+ comment: null,
+ feedback: null,
+ reason: null,
+ },
+ collection_method: "charge_automatically",
+ created: 1770445200,
+ currency: "usd",
+ customer: "cus_TkKmZZvysJ2wej",
+ customer_account: null,
+ days_until_due: null,
+ default_payment_method: null,
+ default_source: "card_1Smq7u2SrMQ2FneknjyOa7sq",
+ default_tax_rates: [],
+ description: null,
+ discounts: [],
+ ended_at: null,
+ invoice_settings: {
+ account_tax_ids: null,
+ issuer: {
+ type: "self",
+ },
+ },
+ items: {
+ object: "list",
+ data: [
+ {
+ id: "si_TkKnBKXFX76t0O",
+ object: "subscription_item",
billing_thresholds: null,
- cancel_at: null,
- cancel_at_period_end: false,
- canceled_at: null,
- cancellation_details: {
- comment: null,
- feedback: null,
- reason: null,
- },
- collection_method: "charge_automatically",
created: 1770445200,
- currency: "usd",
- customer: "cus_TkKmZZvysJ2wej",
- customer_account: null,
- days_until_due: null,
- default_payment_method: null,
- default_source: "card_1Smq7u2SrMQ2FneknjyOa7sq",
- default_tax_rates: [],
- description: null,
+ current_period_end: 1772864400,
+ current_period_start: 1770445200,
discounts: [],
- ended_at: null,
- invoice_settings: {
- account_tax_ids: null,
- issuer: {
- type: "self",
- },
- },
- items: {
- object: "list",
- data: [
- {
- id: "si_TkKnBKXFX76t0O",
- object: "subscription_item",
- billing_thresholds: null,
- created: 1770445200,
- current_period_end: 1772864400,
- current_period_start: 1770445200,
- discounts: [],
- metadata: {},
- plan: {
- id: "price_1SmfFG2SrMQ2FnekJuzwHMea",
- object: "plan",
- active: true,
- amount: 20000,
- amount_decimal: "20000",
- billing_scheme: "per_unit",
- created: 1767725082,
- currency: "usd",
- interval: "month",
- interval_count: 1,
- livemode: false,
- metadata: {},
- meter: null,
- nickname: null,
- product: "prod_Tk9LjWT1n0DgYm",
- tiers_mode: null,
- transform_usage: null,
- trial_period_days: null,
- usage_type: "licensed",
- },
- price: {
- id: "price_1SmfFG2SrMQ2FnekJuzwHMea",
- object: "price",
- active: true,
- billing_scheme: "per_unit",
- created: 1767725082,
- currency: "usd",
- custom_unit_amount: null,
- livemode: false,
- lookup_key: null,
- metadata: {},
- nickname: null,
- product: "prod_Tk9LjWT1n0DgYm",
- recurring: {
- interval: "month",
- interval_count: 1,
- meter: null,
- trial_period_days: null,
- usage_type: "licensed",
- },
- tax_behavior: "unspecified",
- tiers_mode: null,
- transform_quantity: null,
- type: "recurring",
- unit_amount: 20000,
- unit_amount_decimal: "20000",
- },
- quantity: 1,
- subscription: "sub_1Smq7x2SrMQ2Fnek8F1yf3ZD",
- tax_rates: [],
- },
- ],
- has_more: false,
- total_count: 1,
- url: "/v1/subscription_items?subscription=sub_1Smq7x2SrMQ2Fnek8F1yf3ZD",
- },
- latest_invoice: "in_1Smq7x2SrMQ2FnekSJesfPwE",
- livemode: false,
metadata: {},
- next_pending_invoice_item_invoice: null,
- on_behalf_of: null,
- pause_collection: null,
- payment_settings: {
- payment_method_options: null,
- payment_method_types: null,
- save_default_payment_method: "off",
- },
- pending_invoice_item_interval: null,
- pending_setup_intent: null,
- pending_update: null,
plan: {
id: "price_1SmfFG2SrMQ2FnekJuzwHMea",
object: "plan",
@@ -372,29 +302,101 @@ export async function POST(input: APIEvent) {
trial_period_days: null,
usage_type: "licensed",
},
- quantity: 1,
- schedule: null,
- start_date: 1770445200,
- status: "active",
- test_clock: "clock_1Smq6n2SrMQ2FnekQw4yt2PZ",
- transfer_data: null,
- trial_end: null,
- trial_settings: {
- end_behavior: {
- missing_payment_method: "create_invoice",
+ price: {
+ id: "price_1SmfFG2SrMQ2FnekJuzwHMea",
+ object: "price",
+ active: true,
+ billing_scheme: "per_unit",
+ created: 1767725082,
+ currency: "usd",
+ custom_unit_amount: null,
+ livemode: false,
+ lookup_key: null,
+ metadata: {},
+ nickname: null,
+ product: "prod_Tk9LjWT1n0DgYm",
+ recurring: {
+ interval: "month",
+ interval_count: 1,
+ meter: null,
+ trial_period_days: null,
+ usage_type: "licensed",
},
+ tax_behavior: "unspecified",
+ tiers_mode: null,
+ transform_quantity: null,
+ type: "recurring",
+ unit_amount: 20000,
+ unit_amount_decimal: "20000",
},
- trial_start: null,
+ quantity: 1,
+ subscription: "sub_1Smq7x2SrMQ2Fnek8F1yf3ZD",
+ tax_rates: [],
},
- },
+ ],
+ has_more: false,
+ total_count: 1,
+ url: "/v1/subscription_items?subscription=sub_1Smq7x2SrMQ2Fnek8F1yf3ZD",
+ },
+ latest_invoice: "in_1Smq7x2SrMQ2FnekSJesfPwE",
+ livemode: false,
+ metadata: {},
+ next_pending_invoice_item_invoice: null,
+ on_behalf_of: null,
+ pause_collection: null,
+ payment_settings: {
+ payment_method_options: null,
+ payment_method_types: null,
+ save_default_payment_method: "off",
+ },
+ pending_invoice_item_interval: null,
+ pending_setup_intent: null,
+ pending_update: null,
+ plan: {
+ id: "price_1SmfFG2SrMQ2FnekJuzwHMea",
+ object: "plan",
+ active: true,
+ amount: 20000,
+ amount_decimal: "20000",
+ billing_scheme: "per_unit",
+ created: 1767725082,
+ currency: "usd",
+ interval: "month",
+ interval_count: 1,
livemode: false,
- pending_webhooks: 0,
- request: {
- id: "req_6YO9stvB155WJD",
- idempotency_key: "581ba059-6f86-49b2-9c49-0d8450255322",
+ metadata: {},
+ meter: null,
+ nickname: null,
+ product: "prod_Tk9LjWT1n0DgYm",
+ tiers_mode: null,
+ transform_usage: null,
+ trial_period_days: null,
+ usage_type: "licensed",
+ },
+ quantity: 1,
+ schedule: null,
+ start_date: 1770445200,
+ status: "active",
+ test_clock: "clock_1Smq6n2SrMQ2FnekQw4yt2PZ",
+ transfer_data: null,
+ trial_end: null,
+ trial_settings: {
+ end_behavior: {
+ missing_payment_method: "create_invoice",
},
- type: "customer.subscription.created",
- }
+ },
+ trial_start: null,
+ },
+ },
+ livemode: false,
+ pending_webhooks: 0,
+ request: {
+ id: "req_6YO9stvB155WJD",
+ idempotency_key: "581ba059-6f86-49b2-9c49-0d8450255322",
+ },
+ type: "customer.subscription.created",
+}
+ */
}
if (body.type === "customer.subscription.deleted") {
const subscriptionID = body.data.object.id
@@ -419,7 +421,7 @@ export async function POST(input: APIEvent) {
})
}
if (body.type === "invoice.payment_succeeded") {
- if (body.data.object.billing_reason === "subscription_cycle") {
+ if (body.data.object.billing_reason === "subscription_cycle" || body.data.object.billing_reason === "subscription_create") {
const invoiceID = body.data.object.id as string
const amountInCents = body.data.object.amount_paid
const customerID = body.data.object.customer as string
diff --git a/packages/console/app/src/routes/workspace/[id]/billing/black-section.tsx b/packages/console/app/src/routes/workspace/[id]/billing/black-section.tsx
index 98d4debbb..03c75387a 100644
--- a/packages/console/app/src/routes/workspace/[id]/billing/black-section.tsx
+++ b/packages/console/app/src/routes/workspace/[id]/billing/black-section.tsx
@@ -9,6 +9,7 @@ import { Black } from "@opencode-ai/console-core/black.js"
import { withActor } from "~/context/auth.withActor"
import { queryBillingInfo } from "../../common"
import styles from "./black-section.module.css"
+import waitlistStyles from "./black-waitlist-section.module.css"
const querySubscription = query(async (workspaceID: string) => {
"use server"
@@ -27,7 +28,7 @@ const querySubscription = query(async (workspaceID: string) => {
.where(and(eq(SubscriptionTable.workspaceID, Actor.workspace()), isNull(SubscriptionTable.timeDeleted)))
.then((r) => r[0]),
)
- if (!row.subscription) return null
+ if (!row?.subscription) return null
return {
plan: row.subscription.plan,
@@ -58,6 +59,37 @@ function formatResetTime(seconds: number) {
return `${minutes} ${minutes === 1 ? "minute" : "minutes"}`
}
+const cancelWaitlist = action(async (workspaceID: string) => {
+ "use server"
+ return json(
+ await withActor(async () => {
+ await Database.use((tx) =>
+ tx
+ .update(BillingTable)
+ .set({
+ subscriptionPlan: null,
+ timeSubscriptionBooked: null,
+ timeSubscriptionSelected: null,
+ })
+ .where(eq(BillingTable.workspaceID, workspaceID)),
+ )
+ return { error: undefined }
+ }, workspaceID).catch((e) => ({ error: e.message as string })),
+ { revalidate: [queryBillingInfo.key, querySubscription.key] },
+ )
+}, "cancelWaitlist")
+
+const enroll = action(async (workspaceID: string) => {
+ "use server"
+ return json(
+ await withActor(async () => {
+ await Billing.subscribe({ seats: 1 })
+ return { error: undefined }
+ }, workspaceID).catch((e) => ({ error: e.message as string })),
+ { revalidate: [queryBillingInfo.key, querySubscription.key] },
+ )
+}, "enroll")
+
const createSessionUrl = action(async (workspaceID: string, returnUrl: string) => {
"use server"
return json(
@@ -71,17 +103,24 @@ const createSessionUrl = action(async (workspaceID: string, returnUrl: string) =
})),
workspaceID,
),
- { revalidate: queryBillingInfo.key },
+ { revalidate: [queryBillingInfo.key, querySubscription.key] },
)
}, "sessionUrl")
export function BlackSection() {
const params = useParams()
+ const billing = createAsync(() => queryBillingInfo(params.id!))
+ const subscription = createAsync(() => querySubscription(params.id!))
const sessionAction = useAction(createSessionUrl)
const sessionSubmission = useSubmission(createSessionUrl)
- const subscription = createAsync(() => querySubscription(params.id!))
+ const cancelAction = useAction(cancelWaitlist)
+ const cancelSubmission = useSubmission(cancelWaitlist)
+ const enrollAction = useAction(enroll)
+ const enrollSubmission = useSubmission(enroll)
const [store, setStore] = createStore({
sessionRedirecting: false,
+ cancelled: false,
+ enrolled: false,
})
async function onClickSession() {
@@ -92,11 +131,25 @@ export function BlackSection() {
}
}
+ async function onClickCancel() {
+ const result = await cancelAction(params.id!)
+ if (!result.error) {
+ setStore("cancelled", true)
+ }
+ }
+
+ async function onClickEnroll() {
+ const result = await enrollAction(params.id!)
+ if (!result.error) {
+ setStore("enrolled", true)
+ }
+ }
+
return (
-
+ <>
{(sub) => (
- <>
+
Subscription
@@ -132,9 +185,45 @@ export function BlackSection() {
Resets in {formatResetTime(sub().weeklyUsage.resetInSec)}
- >
+
)}
-
+
+
+
+
Waitlist
+
+
+ {billing()?.timeSubscriptionSelected
+ ? `We're ready to enroll you into the $${billing()?.subscriptionPlan} per month OpenCode Black plan.`
+ : `You are on the waitlist for the $${billing()?.subscriptionPlan} per month OpenCode Black plan.`}
+
+
+ {cancelSubmission.pending ? "Leaving..." : store.cancelled ? "Left" : "Leave Waitlist"}
+
+
+
+
+
+
+ {enrollSubmission.pending ? "Enrolling..." : store.enrolled ? "Enrolled" : "Enroll"}
+
+
+ When you click Enroll, your subscription starts immediately and your card will be charged.
+
+
+
+
+
+ >
)
}
diff --git a/packages/console/app/src/routes/workspace/[id]/billing/black-waitlist-section.module.css b/packages/console/app/src/routes/workspace/[id]/billing/black-waitlist-section.module.css
index c189f0d64..685d62c93 100644
--- a/packages/console/app/src/routes/workspace/[id]/billing/black-waitlist-section.module.css
+++ b/packages/console/app/src/routes/workspace/[id]/billing/black-waitlist-section.module.css
@@ -5,4 +5,19 @@
align-items: center;
gap: var(--space-4);
}
+
+ [data-slot="enroll-section"] {
+ display: flex;
+ flex-direction: column;
+ gap: var(--space-3);
+ }
+
+ [data-slot="enroll-button"] {
+ align-self: flex-start;
+ }
+
+ [data-slot="enroll-note"] {
+ font-size: var(--font-size-sm);
+ color: var(--color-text-muted);
+ }
}
diff --git a/packages/console/app/src/routes/workspace/[id]/billing/black-waitlist-section.tsx b/packages/console/app/src/routes/workspace/[id]/billing/black-waitlist-section.tsx
deleted file mode 100644
index 3ec9be395..000000000
--- a/packages/console/app/src/routes/workspace/[id]/billing/black-waitlist-section.tsx
+++ /dev/null
@@ -1,57 +0,0 @@
-import { action, useParams, useAction, useSubmission, json, createAsync } from "@solidjs/router"
-import { createStore } from "solid-js/store"
-import { Database, eq } from "@opencode-ai/console-core/drizzle/index.js"
-import { BillingTable } from "@opencode-ai/console-core/schema/billing.sql.js"
-import { withActor } from "~/context/auth.withActor"
-import { queryBillingInfo } from "../../common"
-import styles from "./black-waitlist-section.module.css"
-
-const cancelWaitlist = action(async (workspaceID: string) => {
- "use server"
- return json(
- await withActor(async () => {
- await Database.use((tx) =>
- tx
- .update(BillingTable)
- .set({
- subscriptionPlan: null,
- timeSubscriptionBooked: null,
- })
- .where(eq(BillingTable.workspaceID, workspaceID)),
- )
- return { error: undefined }
- }, workspaceID).catch((e) => ({ error: e.message as string })),
- { revalidate: queryBillingInfo.key },
- )
-}, "cancelWaitlist")
-
-export function BlackWaitlistSection() {
- const params = useParams()
- const billingInfo = createAsync(() => queryBillingInfo(params.id!))
- const cancelAction = useAction(cancelWaitlist)
- const cancelSubmission = useSubmission(cancelWaitlist)
- const [store, setStore] = createStore({
- cancelled: false,
- })
-
- async function onClickCancel() {
- const result = await cancelAction(params.id!)
- if (!result.error) {
- setStore("cancelled", true)
- }
- }
-
- return (
-
-
-
Waitlist
-
-
You are on the waitlist for the ${billingInfo()?.subscriptionPlan} per month OpenCode Black plan.
-
- {cancelSubmission.pending ? "Leaving..." : store.cancelled ? "Left" : "Leave Waitlist"}
-
-
-
-
- )
-}
diff --git a/packages/console/app/src/routes/workspace/[id]/billing/index.tsx b/packages/console/app/src/routes/workspace/[id]/billing/index.tsx
index 7fe4092be..a252a0234 100644
--- a/packages/console/app/src/routes/workspace/[id]/billing/index.tsx
+++ b/packages/console/app/src/routes/workspace/[id]/billing/index.tsx
@@ -3,7 +3,6 @@ import { BillingSection } from "./billing-section"
import { ReloadSection } from "./reload-section"
import { PaymentSection } from "./payment-section"
import { BlackSection } from "./black-section"
-import { BlackWaitlistSection } from "./black-waitlist-section"
import { Show } from "solid-js"
import { createAsync, useParams } from "@solidjs/router"
import { queryBillingInfo, querySessionInfo } from "../../common"
@@ -17,12 +16,9 @@ export default function () {
-
+
-
-
-
diff --git a/packages/console/app/src/routes/workspace/[id]/index.tsx b/packages/console/app/src/routes/workspace/[id]/index.tsx
index e25e09645..fe308dbb0 100644
--- a/packages/console/app/src/routes/workspace/[id]/index.tsx
+++ b/packages/console/app/src/routes/workspace/[id]/index.tsx
@@ -1,4 +1,4 @@
-import { Show, createMemo } from "solid-js"
+import { Match, Show, Switch, createMemo } from "solid-js"
import { createStore } from "solid-js/store"
import { createAsync, useParams, useAction, useSubmission } from "@solidjs/router"
import { NewUserSection } from "./new-user-section"
@@ -43,9 +43,8 @@ export default function () {
-
+
{checkoutSubmission.pending || store.checkoutRedirecting ? "Loading..." : "Enable billing"}
- }
- >
-
- Current balance ${balance()}
-
-
+
+
+
+ Current balance ${balance()}
+
+
+
diff --git a/packages/console/app/src/routes/workspace/common.tsx b/packages/console/app/src/routes/workspace/common.tsx
index ddd3fd3e6..b5a90f749 100644
--- a/packages/console/app/src/routes/workspace/common.tsx
+++ b/packages/console/app/src/routes/workspace/common.tsx
@@ -113,6 +113,7 @@ export const queryBillingInfo = query(async (workspaceID: string) => {
subscriptionID: billing.subscriptionID,
subscriptionPlan: billing.subscriptionPlan,
timeSubscriptionBooked: billing.timeSubscriptionBooked,
+ timeSubscriptionSelected: billing.timeSubscriptionSelected,
}
}, workspaceID)
}, "billing.get")
diff --git a/packages/console/app/src/routes/zen/util/handler.ts b/packages/console/app/src/routes/zen/util/handler.ts
index 29a6bfb2e..783df8128 100644
--- a/packages/console/app/src/routes/zen/util/handler.ts
+++ b/packages/console/app/src/routes/zen/util/handler.ts
@@ -669,7 +669,7 @@ export async function handler(
...(authInfo.subscription
? (() => {
const plan = authInfo.billing.subscription!.plan
- const black = BlackData.get({ plan })
+ const black = BlackData.getLimits({ plan })
const week = getWeekBounds(new Date())
const rollingWindowSeconds = black.rollingWindow * 3600
return [
diff --git a/packages/console/core/migrations/0055_moaning_karnak.sql b/packages/console/core/migrations/0055_moaning_karnak.sql
new file mode 100644
index 000000000..120ae727c
--- /dev/null
+++ b/packages/console/core/migrations/0055_moaning_karnak.sql
@@ -0,0 +1 @@
+ALTER TABLE `billing` ADD `time_subscription_selected` timestamp(3);
\ No newline at end of file
diff --git a/packages/console/core/migrations/meta/0055_snapshot.json b/packages/console/core/migrations/meta/0055_snapshot.json
new file mode 100644
index 000000000..fadb753b4
--- /dev/null
+++ b/packages/console/core/migrations/meta/0055_snapshot.json
@@ -0,0 +1,1316 @@
+{
+ "version": "5",
+ "dialect": "mysql",
+ "id": "e630f63c-04a8-4b59-bf56-03efcdd1b011",
+ "prevId": "a0ade64b-b735-4a70-8d39-ebd84bc9e924",
+ "tables": {
+ "account": {
+ "name": "account",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "varchar(30)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "time_created": {
+ "name": "time_created",
+ "type": "timestamp(3)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "(now())"
+ },
+ "time_updated": {
+ "name": "time_updated",
+ "type": "timestamp(3)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)"
+ },
+ "time_deleted": {
+ "name": "time_deleted",
+ "type": "timestamp(3)",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {
+ "account_id_pk": {
+ "name": "account_id_pk",
+ "columns": [
+ "id"
+ ]
+ }
+ },
+ "uniqueConstraints": {},
+ "checkConstraint": {}
+ },
+ "auth": {
+ "name": "auth",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "varchar(30)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "time_created": {
+ "name": "time_created",
+ "type": "timestamp(3)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "(now())"
+ },
+ "time_updated": {
+ "name": "time_updated",
+ "type": "timestamp(3)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)"
+ },
+ "time_deleted": {
+ "name": "time_deleted",
+ "type": "timestamp(3)",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "provider": {
+ "name": "provider",
+ "type": "enum('email','github','google')",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "subject": {
+ "name": "subject",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "account_id": {
+ "name": "account_id",
+ "type": "varchar(30)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ }
+ },
+ "indexes": {
+ "provider": {
+ "name": "provider",
+ "columns": [
+ "provider",
+ "subject"
+ ],
+ "isUnique": true
+ },
+ "account_id": {
+ "name": "account_id",
+ "columns": [
+ "account_id"
+ ],
+ "isUnique": false
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {
+ "auth_id_pk": {
+ "name": "auth_id_pk",
+ "columns": [
+ "id"
+ ]
+ }
+ },
+ "uniqueConstraints": {},
+ "checkConstraint": {}
+ },
+ "benchmark": {
+ "name": "benchmark",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "varchar(30)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "time_created": {
+ "name": "time_created",
+ "type": "timestamp(3)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "(now())"
+ },
+ "time_updated": {
+ "name": "time_updated",
+ "type": "timestamp(3)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)"
+ },
+ "time_deleted": {
+ "name": "time_deleted",
+ "type": "timestamp(3)",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "model": {
+ "name": "model",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "agent": {
+ "name": "agent",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "result": {
+ "name": "result",
+ "type": "mediumtext",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ }
+ },
+ "indexes": {
+ "time_created": {
+ "name": "time_created",
+ "columns": [
+ "time_created"
+ ],
+ "isUnique": false
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {
+ "benchmark_id_pk": {
+ "name": "benchmark_id_pk",
+ "columns": [
+ "id"
+ ]
+ }
+ },
+ "uniqueConstraints": {},
+ "checkConstraint": {}
+ },
+ "billing": {
+ "name": "billing",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "varchar(30)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "workspace_id": {
+ "name": "workspace_id",
+ "type": "varchar(30)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "time_created": {
+ "name": "time_created",
+ "type": "timestamp(3)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "(now())"
+ },
+ "time_updated": {
+ "name": "time_updated",
+ "type": "timestamp(3)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)"
+ },
+ "time_deleted": {
+ "name": "time_deleted",
+ "type": "timestamp(3)",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "customer_id": {
+ "name": "customer_id",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "payment_method_id": {
+ "name": "payment_method_id",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "payment_method_type": {
+ "name": "payment_method_type",
+ "type": "varchar(32)",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "payment_method_last4": {
+ "name": "payment_method_last4",
+ "type": "varchar(4)",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "balance": {
+ "name": "balance",
+ "type": "bigint",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "monthly_limit": {
+ "name": "monthly_limit",
+ "type": "int",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "monthly_usage": {
+ "name": "monthly_usage",
+ "type": "bigint",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "time_monthly_usage_updated": {
+ "name": "time_monthly_usage_updated",
+ "type": "timestamp(3)",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "reload": {
+ "name": "reload",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "reload_trigger": {
+ "name": "reload_trigger",
+ "type": "int",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "reload_amount": {
+ "name": "reload_amount",
+ "type": "int",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "reload_error": {
+ "name": "reload_error",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "time_reload_error": {
+ "name": "time_reload_error",
+ "type": "timestamp(3)",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "time_reload_locked_till": {
+ "name": "time_reload_locked_till",
+ "type": "timestamp(3)",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "subscription": {
+ "name": "subscription",
+ "type": "json",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "subscription_id": {
+ "name": "subscription_id",
+ "type": "varchar(28)",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "subscription_plan": {
+ "name": "subscription_plan",
+ "type": "enum('20','100','200')",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "time_subscription_booked": {
+ "name": "time_subscription_booked",
+ "type": "timestamp(3)",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "time_subscription_selected": {
+ "name": "time_subscription_selected",
+ "type": "timestamp(3)",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ }
+ },
+ "indexes": {
+ "global_customer_id": {
+ "name": "global_customer_id",
+ "columns": [
+ "customer_id"
+ ],
+ "isUnique": true
+ },
+ "global_subscription_id": {
+ "name": "global_subscription_id",
+ "columns": [
+ "subscription_id"
+ ],
+ "isUnique": true
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {
+ "billing_workspace_id_id_pk": {
+ "name": "billing_workspace_id_id_pk",
+ "columns": [
+ "workspace_id",
+ "id"
+ ]
+ }
+ },
+ "uniqueConstraints": {},
+ "checkConstraint": {}
+ },
+ "payment": {
+ "name": "payment",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "varchar(30)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "workspace_id": {
+ "name": "workspace_id",
+ "type": "varchar(30)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "time_created": {
+ "name": "time_created",
+ "type": "timestamp(3)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "(now())"
+ },
+ "time_updated": {
+ "name": "time_updated",
+ "type": "timestamp(3)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)"
+ },
+ "time_deleted": {
+ "name": "time_deleted",
+ "type": "timestamp(3)",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "customer_id": {
+ "name": "customer_id",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "invoice_id": {
+ "name": "invoice_id",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "payment_id": {
+ "name": "payment_id",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "amount": {
+ "name": "amount",
+ "type": "bigint",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "time_refunded": {
+ "name": "time_refunded",
+ "type": "timestamp(3)",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "enrichment": {
+ "name": "enrichment",
+ "type": "json",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {
+ "payment_workspace_id_id_pk": {
+ "name": "payment_workspace_id_id_pk",
+ "columns": [
+ "workspace_id",
+ "id"
+ ]
+ }
+ },
+ "uniqueConstraints": {},
+ "checkConstraint": {}
+ },
+ "subscription": {
+ "name": "subscription",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "varchar(30)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "workspace_id": {
+ "name": "workspace_id",
+ "type": "varchar(30)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "time_created": {
+ "name": "time_created",
+ "type": "timestamp(3)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "(now())"
+ },
+ "time_updated": {
+ "name": "time_updated",
+ "type": "timestamp(3)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)"
+ },
+ "time_deleted": {
+ "name": "time_deleted",
+ "type": "timestamp(3)",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "varchar(30)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "rolling_usage": {
+ "name": "rolling_usage",
+ "type": "bigint",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "fixed_usage": {
+ "name": "fixed_usage",
+ "type": "bigint",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "time_rolling_updated": {
+ "name": "time_rolling_updated",
+ "type": "timestamp(3)",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "time_fixed_updated": {
+ "name": "time_fixed_updated",
+ "type": "timestamp(3)",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ }
+ },
+ "indexes": {
+ "workspace_user_id": {
+ "name": "workspace_user_id",
+ "columns": [
+ "workspace_id",
+ "user_id"
+ ],
+ "isUnique": true
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {
+ "subscription_workspace_id_id_pk": {
+ "name": "subscription_workspace_id_id_pk",
+ "columns": [
+ "workspace_id",
+ "id"
+ ]
+ }
+ },
+ "uniqueConstraints": {},
+ "checkConstraint": {}
+ },
+ "usage": {
+ "name": "usage",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "varchar(30)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "workspace_id": {
+ "name": "workspace_id",
+ "type": "varchar(30)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "time_created": {
+ "name": "time_created",
+ "type": "timestamp(3)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "(now())"
+ },
+ "time_updated": {
+ "name": "time_updated",
+ "type": "timestamp(3)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)"
+ },
+ "time_deleted": {
+ "name": "time_deleted",
+ "type": "timestamp(3)",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "model": {
+ "name": "model",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "provider": {
+ "name": "provider",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "input_tokens": {
+ "name": "input_tokens",
+ "type": "int",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "output_tokens": {
+ "name": "output_tokens",
+ "type": "int",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "reasoning_tokens": {
+ "name": "reasoning_tokens",
+ "type": "int",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "cache_read_tokens": {
+ "name": "cache_read_tokens",
+ "type": "int",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "cache_write_5m_tokens": {
+ "name": "cache_write_5m_tokens",
+ "type": "int",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "cache_write_1h_tokens": {
+ "name": "cache_write_1h_tokens",
+ "type": "int",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "cost": {
+ "name": "cost",
+ "type": "bigint",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "key_id": {
+ "name": "key_id",
+ "type": "varchar(30)",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "enrichment": {
+ "name": "enrichment",
+ "type": "json",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ }
+ },
+ "indexes": {
+ "usage_time_created": {
+ "name": "usage_time_created",
+ "columns": [
+ "workspace_id",
+ "time_created"
+ ],
+ "isUnique": false
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {
+ "usage_workspace_id_id_pk": {
+ "name": "usage_workspace_id_id_pk",
+ "columns": [
+ "workspace_id",
+ "id"
+ ]
+ }
+ },
+ "uniqueConstraints": {},
+ "checkConstraint": {}
+ },
+ "ip_rate_limit": {
+ "name": "ip_rate_limit",
+ "columns": {
+ "ip": {
+ "name": "ip",
+ "type": "varchar(45)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "interval": {
+ "name": "interval",
+ "type": "varchar(10)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "count": {
+ "name": "count",
+ "type": "int",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {
+ "ip_rate_limit_ip_interval_pk": {
+ "name": "ip_rate_limit_ip_interval_pk",
+ "columns": [
+ "ip",
+ "interval"
+ ]
+ }
+ },
+ "uniqueConstraints": {},
+ "checkConstraint": {}
+ },
+ "ip": {
+ "name": "ip",
+ "columns": {
+ "ip": {
+ "name": "ip",
+ "type": "varchar(45)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "time_created": {
+ "name": "time_created",
+ "type": "timestamp(3)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "(now())"
+ },
+ "time_updated": {
+ "name": "time_updated",
+ "type": "timestamp(3)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)"
+ },
+ "time_deleted": {
+ "name": "time_deleted",
+ "type": "timestamp(3)",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "usage": {
+ "name": "usage",
+ "type": "int",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {
+ "ip_ip_pk": {
+ "name": "ip_ip_pk",
+ "columns": [
+ "ip"
+ ]
+ }
+ },
+ "uniqueConstraints": {},
+ "checkConstraint": {}
+ },
+ "key": {
+ "name": "key",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "varchar(30)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "workspace_id": {
+ "name": "workspace_id",
+ "type": "varchar(30)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "time_created": {
+ "name": "time_created",
+ "type": "timestamp(3)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "(now())"
+ },
+ "time_updated": {
+ "name": "time_updated",
+ "type": "timestamp(3)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)"
+ },
+ "time_deleted": {
+ "name": "time_deleted",
+ "type": "timestamp(3)",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "name": {
+ "name": "name",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "key": {
+ "name": "key",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "varchar(30)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "time_used": {
+ "name": "time_used",
+ "type": "timestamp(3)",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ }
+ },
+ "indexes": {
+ "global_key": {
+ "name": "global_key",
+ "columns": [
+ "key"
+ ],
+ "isUnique": true
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {
+ "key_workspace_id_id_pk": {
+ "name": "key_workspace_id_id_pk",
+ "columns": [
+ "workspace_id",
+ "id"
+ ]
+ }
+ },
+ "uniqueConstraints": {},
+ "checkConstraint": {}
+ },
+ "model": {
+ "name": "model",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "varchar(30)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "workspace_id": {
+ "name": "workspace_id",
+ "type": "varchar(30)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "time_created": {
+ "name": "time_created",
+ "type": "timestamp(3)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "(now())"
+ },
+ "time_updated": {
+ "name": "time_updated",
+ "type": "timestamp(3)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)"
+ },
+ "time_deleted": {
+ "name": "time_deleted",
+ "type": "timestamp(3)",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "model": {
+ "name": "model",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ }
+ },
+ "indexes": {
+ "model_workspace_model": {
+ "name": "model_workspace_model",
+ "columns": [
+ "workspace_id",
+ "model"
+ ],
+ "isUnique": true
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {
+ "model_workspace_id_id_pk": {
+ "name": "model_workspace_id_id_pk",
+ "columns": [
+ "workspace_id",
+ "id"
+ ]
+ }
+ },
+ "uniqueConstraints": {},
+ "checkConstraint": {}
+ },
+ "provider": {
+ "name": "provider",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "varchar(30)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "workspace_id": {
+ "name": "workspace_id",
+ "type": "varchar(30)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "time_created": {
+ "name": "time_created",
+ "type": "timestamp(3)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "(now())"
+ },
+ "time_updated": {
+ "name": "time_updated",
+ "type": "timestamp(3)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)"
+ },
+ "time_deleted": {
+ "name": "time_deleted",
+ "type": "timestamp(3)",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "provider": {
+ "name": "provider",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "credentials": {
+ "name": "credentials",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ }
+ },
+ "indexes": {
+ "workspace_provider": {
+ "name": "workspace_provider",
+ "columns": [
+ "workspace_id",
+ "provider"
+ ],
+ "isUnique": true
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {
+ "provider_workspace_id_id_pk": {
+ "name": "provider_workspace_id_id_pk",
+ "columns": [
+ "workspace_id",
+ "id"
+ ]
+ }
+ },
+ "uniqueConstraints": {},
+ "checkConstraint": {}
+ },
+ "user": {
+ "name": "user",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "varchar(30)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "workspace_id": {
+ "name": "workspace_id",
+ "type": "varchar(30)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "time_created": {
+ "name": "time_created",
+ "type": "timestamp(3)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "(now())"
+ },
+ "time_updated": {
+ "name": "time_updated",
+ "type": "timestamp(3)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)"
+ },
+ "time_deleted": {
+ "name": "time_deleted",
+ "type": "timestamp(3)",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "account_id": {
+ "name": "account_id",
+ "type": "varchar(30)",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "email": {
+ "name": "email",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "name": {
+ "name": "name",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "time_seen": {
+ "name": "time_seen",
+ "type": "timestamp(3)",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "color": {
+ "name": "color",
+ "type": "int",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "role": {
+ "name": "role",
+ "type": "enum('admin','member')",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "monthly_limit": {
+ "name": "monthly_limit",
+ "type": "int",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "monthly_usage": {
+ "name": "monthly_usage",
+ "type": "bigint",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "time_monthly_usage_updated": {
+ "name": "time_monthly_usage_updated",
+ "type": "timestamp(3)",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ }
+ },
+ "indexes": {
+ "user_account_id": {
+ "name": "user_account_id",
+ "columns": [
+ "workspace_id",
+ "account_id"
+ ],
+ "isUnique": true
+ },
+ "user_email": {
+ "name": "user_email",
+ "columns": [
+ "workspace_id",
+ "email"
+ ],
+ "isUnique": true
+ },
+ "global_account_id": {
+ "name": "global_account_id",
+ "columns": [
+ "account_id"
+ ],
+ "isUnique": false
+ },
+ "global_email": {
+ "name": "global_email",
+ "columns": [
+ "email"
+ ],
+ "isUnique": false
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {
+ "user_workspace_id_id_pk": {
+ "name": "user_workspace_id_id_pk",
+ "columns": [
+ "workspace_id",
+ "id"
+ ]
+ }
+ },
+ "uniqueConstraints": {},
+ "checkConstraint": {}
+ },
+ "workspace": {
+ "name": "workspace",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "varchar(30)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "slug": {
+ "name": "slug",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "name": {
+ "name": "name",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "time_created": {
+ "name": "time_created",
+ "type": "timestamp(3)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "(now())"
+ },
+ "time_updated": {
+ "name": "time_updated",
+ "type": "timestamp(3)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)"
+ },
+ "time_deleted": {
+ "name": "time_deleted",
+ "type": "timestamp(3)",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ }
+ },
+ "indexes": {
+ "slug": {
+ "name": "slug",
+ "columns": [
+ "slug"
+ ],
+ "isUnique": true
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {
+ "workspace_id": {
+ "name": "workspace_id",
+ "columns": [
+ "id"
+ ]
+ }
+ },
+ "uniqueConstraints": {},
+ "checkConstraint": {}
+ }
+ },
+ "views": {},
+ "_meta": {
+ "schemas": {},
+ "tables": {},
+ "columns": {}
+ },
+ "internal": {
+ "tables": {},
+ "indexes": {}
+ }
+}
\ No newline at end of file
diff --git a/packages/console/core/migrations/meta/_journal.json b/packages/console/core/migrations/meta/_journal.json
index dd0957e51..0314e38c9 100644
--- a/packages/console/core/migrations/meta/_journal.json
+++ b/packages/console/core/migrations/meta/_journal.json
@@ -386,6 +386,13 @@
"when": 1768603665356,
"tag": "0054_numerous_annihilus",
"breakpoints": true
+ },
+ {
+ "idx": 55,
+ "version": "5",
+ "when": 1769108945841,
+ "tag": "0055_moaning_karnak",
+ "breakpoints": true
}
]
-}
+}
\ No newline at end of file
diff --git a/packages/console/core/script/black-gift.ts b/packages/console/core/script/black-gift.ts
index ec7db8799..c666a1ab6 100644
--- a/packages/console/core/script/black-gift.ts
+++ b/packages/console/core/script/black-gift.ts
@@ -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}`)
diff --git a/packages/console/core/script/black-onboard.ts b/packages/console/core/script/black-onboard.ts
deleted file mode 100644
index 77e5b779e..000000000
--- a/packages/console/core/script/black-onboard.ts
+++ /dev/null
@@ -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 ")
- 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/lookup-user.ts b/packages/console/core/script/lookup-user.ts
index 6c76d42c9..0a614bc15 100644
--- a/packages/console/core/script/lookup-user.ts
+++ b/packages/console/core/script/lookup-user.ts
@@ -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)
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),