summaryrefslogtreecommitdiffhomepage
path: root/cloud/core/src
diff options
context:
space:
mode:
authorFrank <[email protected]>2025-09-15 14:48:00 -0400
committerFrank <[email protected]>2025-09-15 14:48:02 -0400
commit5e6dd312eb9e4883b43fe9e61f9df7677e880812 (patch)
tree429d5eae2a4aca9af94b2e44ded090794e9dd693 /cloud/core/src
parent7218a662ab08e5526d469090a565342872f04a48 (diff)
downloadopencode-5e6dd312eb9e4883b43fe9e61f9df7677e880812.tar.gz
opencode-5e6dd312eb9e4883b43fe9e61f9df7677e880812.zip
wip: zen
Diffstat (limited to 'cloud/core/src')
-rw-r--r--cloud/core/src/billing.ts113
-rw-r--r--cloud/core/src/schema/billing.sql.ts12
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(