Frank 1 месяц назад
Родитель
Сommit
5f3ab9395f

+ 152 - 150
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

+ 96 - 7
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 (
-    <section class={styles.root}>
+    <>
       <Show when={subscription()}>
         {(sub) => (
-          <>
+          <section class={styles.root}>
             <div data-slot="section-title">
               <h2>Subscription</h2>
               <div data-slot="title-row">
@@ -132,9 +185,45 @@ export function BlackSection() {
                 <span data-slot="reset-time">Resets in {formatResetTime(sub().weeklyUsage.resetInSec)}</span>
               </div>
             </div>
-          </>
+          </section>
         )}
       </Show>
-    </section>
+      <Show when={billing()?.timeSubscriptionBooked}>
+        <section class={waitlistStyles.root}>
+          <div data-slot="section-title">
+            <h2>Waitlist</h2>
+            <div data-slot="title-row">
+              <p>
+                {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.`}
+              </p>
+              <button
+                data-color="danger"
+                disabled={cancelSubmission.pending || store.cancelled}
+                onClick={onClickCancel}
+              >
+                {cancelSubmission.pending ? "Leaving..." : store.cancelled ? "Left" : "Leave Waitlist"}
+              </button>
+            </div>
+          </div>
+          <Show when={billing()?.timeSubscriptionSelected}>
+            <div data-slot="enroll-section">
+              <button
+                data-slot="enroll-button"
+                data-color="primary"
+                disabled={enrollSubmission.pending || store.enrolled}
+                onClick={onClickEnroll}
+              >
+                {enrollSubmission.pending ? "Enrolling..." : store.enrolled ? "Enrolled" : "Enroll"}
+              </button>
+              <p data-slot="enroll-note">
+                When you click Enroll, your subscription starts immediately and your card will be charged.
+              </p>
+            </div>
+          </Show>
+        </section>
+      </Show>
+    </>
   )
 }

+ 15 - 0
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);
+  }
 }

+ 0 - 57
packages/console/app/src/routes/workspace/[id]/billing/black-waitlist-section.tsx

@@ -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 (
-    <section class={styles.root}>
-      <div data-slot="section-title">
-        <h2>Waitlist</h2>
-        <div data-slot="title-row">
-          <p>You are on the waitlist for the ${billingInfo()?.subscriptionPlan} per month OpenCode Black plan.</p>
-          <button data-color="danger" disabled={cancelSubmission.pending || store.cancelled} onClick={onClickCancel}>
-            {cancelSubmission.pending ? "Leaving..." : store.cancelled ? "Left" : "Leave Waitlist"}
-          </button>
-        </div>
-      </div>
-    </section>
-  )
-}

+ 1 - 5
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 () {
     <div data-page="workspace-[id]">
       <div data-slot="sections">
         <Show when={sessionInfo()?.isAdmin}>
-          <Show when={billingInfo()?.subscriptionID}>
+          <Show when={billingInfo()?.subscriptionID || billingInfo()?.timeSubscriptionBooked}>
             <BlackSection />
           </Show>
-          <Show when={billingInfo()?.timeSubscriptionBooked}>
-            <BlackWaitlistSection />
-          </Show>
           <BillingSection />
           <Show when={billingInfo()?.customerID}>
             <ReloadSection />

+ 10 - 10
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 () {
           </span>
           <Show when={userInfo()?.isAdmin}>
             <span data-slot="billing-info">
-              <Show
-                when={billingInfo()?.reload}
-                fallback={
+              <Switch>
+                <Match when={!billingInfo()?.customerID}>
                   <button
                     data-color="primary"
                     data-size="sm"
@@ -54,12 +53,13 @@ export default function () {
                   >
                     {checkoutSubmission.pending || store.checkoutRedirecting ? "Loading..." : "Enable billing"}
                   </button>
-                }
-              >
-                <span data-slot="balance">
-                  Current balance <b>${balance()}</b>
-                </span>
-              </Show>
+                </Match>
+                <Match when={!billingInfo()?.subscriptionID}>
+                  <span data-slot="balance">
+                    Current balance <b>${balance()}</b>
+                  </span>
+                </Match>
+              </Switch>
             </span>
           </Show>
         </p>

+ 1 - 0
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")

+ 1 - 1
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 [

+ 1 - 0
packages/console/core/migrations/0055_moaning_karnak.sql

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

+ 1316 - 0
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": {}
+  }
+}

+ 8 - 1
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
     }
   ]
-}
+}

+ 7 - 2
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}`)
 

+ 0 - 173
packages/console/core/script/black-onboard.ts

@@ -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)"}`)

+ 1 - 1
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)
 

+ 64 - 1
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
+  })
 }

+ 23 - 10
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)

+ 1 - 0
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),