summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--infra/console.ts1
-rw-r--r--packages/console/app/src/routes/stripe/webhook.ts244
-rw-r--r--packages/console/app/src/routes/workspace/[id]/billing/black-section.module.css6
-rw-r--r--packages/console/app/src/routes/workspace/[id]/billing/black-section.tsx50
-rw-r--r--packages/console/core/migrations/0045_cuddly_diamondback.sql1
-rw-r--r--packages/console/core/migrations/meta/0045_snapshot.json1217
-rw-r--r--packages/console/core/migrations/meta/_journal.json9
-rw-r--r--packages/console/core/script/onboard-zen-black.ts16
-rw-r--r--packages/console/core/src/schema/billing.sql.ts6
9 files changed, 1545 insertions, 5 deletions
diff --git a/infra/console.ts b/infra/console.ts
index 2ad577fc6..1e584ca57 100644
--- a/infra/console.ts
+++ b/infra/console.ts
@@ -76,6 +76,7 @@ export const stripeWebhook = new stripe.WebhookEndpoint("StripeWebhookEndpoint",
"checkout.session.completed",
"checkout.session.expired",
"charge.refunded",
+ "invoice.payment_succeeded",
"customer.created",
"customer.deleted",
"customer.updated",
diff --git a/packages/console/app/src/routes/stripe/webhook.ts b/packages/console/app/src/routes/stripe/webhook.ts
index 3260f31b2..8b15d56b5 100644
--- a/packages/console/app/src/routes/stripe/webhook.ts
+++ b/packages/console/app/src/routes/stripe/webhook.ts
@@ -2,6 +2,7 @@ import { Billing } from "@opencode-ai/console-core/billing.js"
import type { APIEvent } from "@solidjs/start/server"
import { and, Database, eq, sql } from "@opencode-ai/console-core/drizzle/index.js"
import { BillingTable, PaymentTable } from "@opencode-ai/console-core/schema/billing.sql.js"
+import { UserTable } from "@opencode-ai/console-core/schema/user.sql.js"
import { Identifier } from "@opencode-ai/console-core/identifier.js"
import { centsToMicroCents } from "@opencode-ai/console-core/util/price.js"
import { Actor } from "@opencode-ai/console-core/actor.js"
@@ -146,6 +147,249 @@ export async function POST(input: APIEvent) {
.where(eq(BillingTable.workspaceID, workspaceID))
})
}
+ if (body.type === "invoice.payment_succeeded") {
+ const invoice = body.data.object
+ if (invoice.billing_reason === "subscription_cycle") {
+ const invoiceID = invoice.id as string
+ const amountInCents = invoice.amount_paid
+ const customerID = invoice.customer as string
+ const subscriptionID = invoice.parent?.subscription_details?.subscription as string
+
+ if (!customerID) throw new Error("Customer ID not found")
+ if (!invoiceID) throw new Error("Invoice ID not found")
+ if (!subscriptionID) throw new Error("Subscription ID not found")
+
+ const payment = await Billing.stripe().invoicePayments.retrieve(invoiceID)
+ const paymentID = payment.id as string
+ if (!paymentID) throw new Error("Payment ID not found")
+
+ const workspaceID = await Database.use((tx) =>
+ tx
+ .select({ workspaceID: BillingTable.workspaceID })
+ .from(BillingTable)
+ .where(eq(BillingTable.customerID, customerID))
+ .then((rows) => rows[0]?.workspaceID),
+ )
+ if (!workspaceID) throw new Error("Workspace ID not found for customer")
+
+ await Database.transaction(async (tx) => {
+ await tx
+ .update(BillingTable)
+ .set({
+ balance: sql`${BillingTable.balance} + ${centsToMicroCents(amountInCents)}`,
+ })
+ .where(eq(BillingTable.workspaceID, workspaceID))
+ await tx.insert(PaymentTable).values({
+ workspaceID,
+ id: Identifier.create("payment"),
+ amount: centsToMicroCents(amountInCents),
+ paymentID,
+ invoiceID,
+ customerID,
+ })
+ })
+ }
+ }
+ 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,
+ },
+ 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,
+ 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",
+ 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",
+ },
+ 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",
+ },
+ },
+ 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
+ if (!subscriptionID) throw new Error("Subscription ID not found")
+
+ const workspaceID = await Database.use((tx) =>
+ tx
+ .select({ workspaceID: BillingTable.workspaceID })
+ .from(BillingTable)
+ .where(eq(BillingTable.subscriptionID, subscriptionID))
+ .then((rows) => rows[0]?.workspaceID),
+ )
+ if (!workspaceID) throw new Error("Workspace ID not found for subscription")
+
+ await Database.transaction(async (tx) => {
+ await tx.update(BillingTable).set({ subscriptionID: null }).where(eq(BillingTable.workspaceID, workspaceID))
+
+ await tx.update(UserTable).set({ timeSubscribed: null }).where(eq(UserTable.workspaceID, workspaceID))
+ })
+ }
})()
.then((message) => {
return Response.json({ message: message ?? "done" }, { status: 200 })
diff --git a/packages/console/app/src/routes/workspace/[id]/billing/black-section.module.css b/packages/console/app/src/routes/workspace/[id]/billing/black-section.module.css
index c3a2af639..c189f0d64 100644
--- a/packages/console/app/src/routes/workspace/[id]/billing/black-section.module.css
+++ b/packages/console/app/src/routes/workspace/[id]/billing/black-section.module.css
@@ -1,2 +1,8 @@
.root {
+ [data-slot="title-row"] {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ gap: var(--space-4);
+ }
}
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 d4876b242..2eece1b62 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
@@ -1,11 +1,57 @@
+import { action, useParams, useAction, useSubmission, json } from "@solidjs/router"
+import { createStore } from "solid-js/store"
+import { Billing } from "@opencode-ai/console-core/billing.js"
+import { withActor } from "~/context/auth.withActor"
+import { queryBillingInfo } from "../../common"
import styles from "./black-section.module.css"
+const createSessionUrl = action(async (workspaceID: string, returnUrl: string) => {
+ "use server"
+ return json(
+ await withActor(
+ () =>
+ Billing.generateSessionUrl({ returnUrl })
+ .then((data) => ({ error: undefined, data }))
+ .catch((e) => ({
+ error: e.message as string,
+ data: undefined,
+ })),
+ workspaceID,
+ ),
+ { revalidate: queryBillingInfo.key },
+ )
+}, "sessionUrl")
+
export function BlackSection() {
+ const params = useParams()
+ const sessionAction = useAction(createSessionUrl)
+ const sessionSubmission = useSubmission(createSessionUrl)
+ const [store, setStore] = createStore({
+ sessionRedirecting: false,
+ })
+
+ async function onClickSession() {
+ const result = await sessionAction(params.id!, window.location.href)
+ if (result.data) {
+ setStore("sessionRedirecting", true)
+ window.location.href = result.data
+ }
+ }
+
return (
<section class={styles.root}>
<div data-slot="section-title">
- <h2>Black</h2>
- <p>You are subscribed to Black.</p>
+ <h2>Subscription</h2>
+ <div data-slot="title-row">
+ <p>You are subscribed to OpenCode Black for $200 per month.</p>
+ <button
+ data-color="primary"
+ disabled={sessionSubmission.pending || store.sessionRedirecting}
+ onClick={onClickSession}
+ >
+ {sessionSubmission.pending || store.sessionRedirecting ? "Loading..." : "Manage Subscription"}
+ </button>
+ </div>
</div>
</section>
)
diff --git a/packages/console/core/migrations/0045_cuddly_diamondback.sql b/packages/console/core/migrations/0045_cuddly_diamondback.sql
new file mode 100644
index 000000000..476e6bfe8
--- /dev/null
+++ b/packages/console/core/migrations/0045_cuddly_diamondback.sql
@@ -0,0 +1 @@
+ALTER TABLE `billing` ADD CONSTRAINT `global_subscription_id` UNIQUE(`subscription_id`); \ No newline at end of file
diff --git a/packages/console/core/migrations/meta/0045_snapshot.json b/packages/console/core/migrations/meta/0045_snapshot.json
new file mode 100644
index 000000000..c27b2d583
--- /dev/null
+++ b/packages/console/core/migrations/meta/0045_snapshot.json
@@ -0,0 +1,1217 @@
+{
+ "version": "5",
+ "dialect": "mysql",
+ "id": "27c1a3eb-b125-46d4-b436-abe5764fe4b7",
+ "prevId": "70394850-2c28-4012-a3d5-69357e3348b6",
+ "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_id": {
+ "name": "subscription_id",
+ "type": "varchar(28)",
+ "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
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {
+ "payment_workspace_id_id_pk": {
+ "name": "payment_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
+ },
+ "time_subscribed": {
+ "name": "time_subscribed",
+ "type": "timestamp(3)",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "sub_interval_usage": {
+ "name": "sub_interval_usage",
+ "type": "bigint",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "sub_monthly_usage": {
+ "name": "sub_monthly_usage",
+ "type": "bigint",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "sub_time_interval_usage_updated": {
+ "name": "sub_time_interval_usage_updated",
+ "type": "timestamp(3)",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "sub_time_monthly_usage_updated": {
+ "name": "sub_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 5244df71a..26efe42b7 100644
--- a/packages/console/core/migrations/meta/_journal.json
+++ b/packages/console/core/migrations/meta/_journal.json
@@ -316,6 +316,13 @@
"when": 1767759322451,
"tag": "0044_tiny_captain_midlands",
"breakpoints": true
+ },
+ {
+ "idx": 45,
+ "version": "5",
+ "when": 1767765497502,
+ "tag": "0045_cuddly_diamondback",
+ "breakpoints": true
}
]
-}
+} \ No newline at end of file
diff --git a/packages/console/core/script/onboard-zen-black.ts b/packages/console/core/script/onboard-zen-black.ts
index f108c934b..15418af2f 100644
--- a/packages/console/core/script/onboard-zen-black.ts
+++ b/packages/console/core/script/onboard-zen-black.ts
@@ -82,6 +82,14 @@ 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 by email via AuthTable
const auth = await Database.use((tx) =>
tx
@@ -116,12 +124,15 @@ await Billing.stripe().customers.update(customerID, {
})
await Database.transaction(async (tx) => {
- // Set customer id and subscription id on workspace billing
+ // Set customer id, subscription id, and payment method on workspace billing
await tx
.update(BillingTable)
.set({
customerID,
subscriptionID,
+ paymentMethodID,
+ paymentMethodLast4,
+ paymentMethodType,
})
.where(eq(BillingTable.workspaceID, workspaceID))
@@ -147,6 +158,9 @@ await Database.transaction(async (tx) => {
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/src/schema/billing.sql.ts b/packages/console/core/src/schema/billing.sql.ts
index 8210bc152..42da13776 100644
--- a/packages/console/core/src/schema/billing.sql.ts
+++ b/packages/console/core/src/schema/billing.sql.ts
@@ -23,7 +23,11 @@ export const BillingTable = mysqlTable(
timeReloadLockedTill: utc("time_reload_locked_till"),
subscriptionID: varchar("subscription_id", { length: 28 }),
},
- (table) => [...workspaceIndexes(table), uniqueIndex("global_customer_id").on(table.customerID)],
+ (table) => [
+ ...workspaceIndexes(table),
+ uniqueIndex("global_customer_id").on(table.customerID),
+ uniqueIndex("global_subscription_id").on(table.subscriptionID),
+ ],
)
export const PaymentTable = mysqlTable(