mirror of
https://github.com/aljazceru/opencode.git
synced 2026-01-18 23:34:55 +01:00
wip: zen
This commit is contained in:
@@ -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(),
|
||||
}),
|
||||
|
||||
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user