diff --git a/infra/console.ts b/infra/console.ts index 2ad577fc69..1e584ca576 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 3260f31b22..8b15d56b51 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 c3a2af6391..c189f0d646 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 d4876b242e..2eece1b62d 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 (
-

Black

-

You are subscribed to Black.

+

Subscription

+
+

You are subscribed to OpenCode Black for $200 per month.

+ +
) 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 0000000000..476e6bfe8a --- /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 0000000000..c27b2d583a --- /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 5244df71ae..26efe42b7e 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 f108c934bd..15418af2fc 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 8210bc1529..42da137769 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(