diff options
| author | Frank <[email protected]> | 2025-09-15 14:48:00 -0400 |
|---|---|---|
| committer | Frank <[email protected]> | 2025-09-15 14:48:02 -0400 |
| commit | 5e6dd312eb9e4883b43fe9e61f9df7677e880812 (patch) | |
| tree | 429d5eae2a4aca9af94b2e44ded090794e9dd693 /cloud/core/src | |
| parent | 7218a662ab08e5526d469090a565342872f04a48 (diff) | |
| download | opencode-5e6dd312eb9e4883b43fe9e61f9df7677e880812.tar.gz opencode-5e6dd312eb9e4883b43fe9e61f9df7677e880812.zip | |
wip: zen
Diffstat (limited to 'cloud/core/src')
| -rw-r--r-- | cloud/core/src/billing.ts | 113 | ||||
| -rw-r--r-- | cloud/core/src/schema/billing.sql.ts | 12 |
2 files changed, 119 insertions, 6 deletions
diff --git a/cloud/core/src/billing.ts b/cloud/core/src/billing.ts index 1677d159d..cbf064bfe 100644 --- a/cloud/core/src/billing.ts +++ b/cloud/core/src/billing.ts @@ -6,8 +6,13 @@ import { fn } from "./util/fn" import { z } from "zod" import { User } from "./user" import { Resource } from "@opencode/cloud-resource" +import { Identifier } from "./identifier" +import { centsToMicroCents } from "./util/price" export namespace Billing { + export const CHARGE_AMOUNT = 2000 // $20 + export const CHARGE_FEE = 123 // Stripe fee 4.4% + $0.30 + export const CHARGE_THRESHOLD = 500 // $5 export const stripe = () => new Stripe(Resource.STRIPE_SECRET_KEY.value, { apiVersion: "2025-03-31.basil", @@ -19,8 +24,14 @@ export namespace Billing { .select({ customerID: BillingTable.customerID, paymentMethodID: BillingTable.paymentMethodID, + paymentMethodLast4: BillingTable.paymentMethodLast4, balance: BillingTable.balance, reload: BillingTable.reload, + monthlyLimit: BillingTable.monthlyLimit, + monthlyUsage: BillingTable.monthlyUsage, + timeMonthlyUsageUpdated: BillingTable.timeMonthlyUsageUpdated, + reloadError: BillingTable.reloadError, + timeReloadError: BillingTable.timeReloadError, }) .from(BillingTable) .where(eq(BillingTable.workspaceID, Actor.workspace())) @@ -50,6 +61,87 @@ export namespace Billing { ) } + export const reload = async () => { + const { customerID, paymentMethodID } = await Database.use((tx) => + tx + .select({ + customerID: BillingTable.customerID, + paymentMethodID: BillingTable.paymentMethodID, + }) + .from(BillingTable) + .where(eq(BillingTable.workspaceID, Actor.workspace())) + .then((rows) => rows[0]), + ) + const paymentID = Identifier.create("payment") + let charge + try { + charge = await Billing.stripe().paymentIntents.create( + { + amount: Billing.CHARGE_AMOUNT + Billing.CHARGE_FEE, + currency: "usd", + customer: customerID!, + payment_method: paymentMethodID!, + off_session: true, + confirm: true, + }, + { idempotencyKey: paymentID }, + ) + + if (charge.status !== "succeeded") throw new Error(charge.last_payment_error?.message) + } catch (e: any) { + await Database.use((tx) => + tx + .update(BillingTable) + .set({ + reloadError: e.message ?? "Payment failed.", + timeReloadError: sql`now()`, + }) + .where(eq(BillingTable.workspaceID, Actor.workspace())), + ) + return + } + + await Database.transaction(async (tx) => { + await tx + .update(BillingTable) + .set({ + balance: sql`${BillingTable.balance} + ${centsToMicroCents(CHARGE_AMOUNT)}`, + reloadError: null, + timeReloadError: null, + }) + .where(eq(BillingTable.workspaceID, Actor.workspace())) + await tx.insert(PaymentTable).values({ + workspaceID: Actor.workspace(), + id: paymentID, + amount: centsToMicroCents(CHARGE_AMOUNT), + paymentID: charge.id, + customerID, + }) + }) + } + + export const disableReload = async () => { + return await Database.use((tx) => + tx + .update(BillingTable) + .set({ + reload: false, + }) + .where(eq(BillingTable.workspaceID, Actor.workspace())), + ) + } + + export const setMonthlyLimit = fn(z.number(), async (input) => { + return await Database.use((tx) => + tx + .update(BillingTable) + .set({ + monthlyLimit: input, + }) + .where(eq(BillingTable.workspaceID, Actor.workspace())), + ) + }) + export const generateCheckoutUrl = fn( z.object({ successUrl: z.string(), @@ -70,7 +162,17 @@ export namespace Billing { product_data: { name: "opencode credits", }, - unit_amount: 2123, // $20 minimum + Stripe fee 4.4% + $0.30 + unit_amount: CHARGE_AMOUNT, + }, + quantity: 1, + }, + { + price_data: { + currency: "usd", + product_data: { + name: "processing fee", + }, + unit_amount: CHARGE_FEE, }, quantity: 1, }, @@ -79,7 +181,9 @@ export namespace Billing { setup_future_usage: "on_session", }, ...(customer.customerID - ? { customer: customer.customerID } + ? { + customer: customer.customerID, + } : { customer_email: user.email, customer_creation: "always", @@ -89,6 +193,9 @@ export namespace Billing { }, currency: "usd", payment_method_types: ["card"], + payment_method_data: { + allow_redisplay: "always", + }, success_url: successUrl, cancel_url: cancelUrl, }) @@ -97,7 +204,7 @@ export namespace Billing { }, ) - export const generatePortalUrl = fn( + export const generateSessionUrl = fn( z.object({ returnUrl: z.string(), }), diff --git a/cloud/core/src/schema/billing.sql.ts b/cloud/core/src/schema/billing.sql.ts index 7d73b922c..5bec4e900 100644 --- a/cloud/core/src/schema/billing.sql.ts +++ b/cloud/core/src/schema/billing.sql.ts @@ -1,5 +1,5 @@ -import { bigint, boolean, int, mysqlTable, varchar, json } from "drizzle-orm/mysql-core" -import { timestamps, workspaceColumns } from "../drizzle/types" +import { bigint, boolean, int, mysqlTable, uniqueIndex, varchar } from "drizzle-orm/mysql-core" +import { timestamps, utc, workspaceColumns } from "../drizzle/types" import { workspaceIndexes } from "./workspace.sql" export const BillingTable = mysqlTable( @@ -11,9 +11,15 @@ export const BillingTable = mysqlTable( paymentMethodID: varchar("payment_method_id", { length: 255 }), paymentMethodLast4: varchar("payment_method_last4", { length: 4 }), balance: bigint("balance", { mode: "number" }).notNull(), + monthlyLimit: int("monthly_limit"), + monthlyUsage: bigint("monthly_usage", { mode: "number" }), + timeMonthlyUsageUpdated: utc("time_monthly_usage_updated"), reload: boolean("reload"), + reloadError: varchar("reload_error", { length: 255 }), + timeReloadError: utc("time_reload_error"), + timeReloadLockedTill: utc("time_reload_locked_till"), }, - (table) => [...workspaceIndexes(table)], + (table) => [...workspaceIndexes(table), uniqueIndex("global_customer_id").on(table.customerID)], ) export const PaymentTable = mysqlTable( |
