billing.ts 4.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162
  1. import { Stripe } from "stripe"
  2. import { Database, eq, sql } from "./drizzle"
  3. import { BillingTable, PaymentTable, UsageTable } from "./schema/billing.sql"
  4. import { Actor } from "./actor"
  5. import { fn } from "./util/fn"
  6. import { z } from "zod"
  7. import { Identifier } from "./identifier"
  8. import { centsToMicroCents } from "./util/price"
  9. import { User } from "./user"
  10. import { Resource } from "./util/resource"
  11. export namespace Billing {
  12. export const stripe = () =>
  13. new Stripe(Resource.STRIPE_SECRET_KEY.value, {
  14. apiVersion: "2025-03-31.basil",
  15. })
  16. export const get = async () => {
  17. return Database.use(async (tx) =>
  18. tx
  19. .select({
  20. customerID: BillingTable.customerID,
  21. paymentMethodID: BillingTable.paymentMethodID,
  22. balance: BillingTable.balance,
  23. reload: BillingTable.reload,
  24. })
  25. .from(BillingTable)
  26. .where(eq(BillingTable.workspaceID, Actor.workspace()))
  27. .then((r) => r[0]),
  28. )
  29. }
  30. export const payments = async () => {
  31. return await Database.use((tx) =>
  32. tx
  33. .select()
  34. .from(PaymentTable)
  35. .where(eq(PaymentTable.workspaceID, Actor.workspace()))
  36. .orderBy(sql`${PaymentTable.timeCreated} DESC`)
  37. .limit(100),
  38. )
  39. }
  40. export const usages = async () => {
  41. return await Database.use((tx) =>
  42. tx
  43. .select()
  44. .from(UsageTable)
  45. .where(eq(UsageTable.workspaceID, Actor.workspace()))
  46. .orderBy(sql`${UsageTable.timeCreated} DESC`)
  47. .limit(100),
  48. )
  49. }
  50. export const consume = fn(
  51. z.object({
  52. requestID: z.string().optional(),
  53. model: z.string(),
  54. inputTokens: z.number(),
  55. outputTokens: z.number(),
  56. reasoningTokens: z.number().optional(),
  57. cacheReadTokens: z.number().optional(),
  58. cacheWriteTokens: z.number().optional(),
  59. costInCents: z.number(),
  60. }),
  61. async (input) => {
  62. const workspaceID = Actor.workspace()
  63. const cost = centsToMicroCents(input.costInCents)
  64. return await Database.transaction(async (tx) => {
  65. await tx.insert(UsageTable).values({
  66. workspaceID,
  67. id: Identifier.create("usage"),
  68. requestID: input.requestID,
  69. model: input.model,
  70. inputTokens: input.inputTokens,
  71. outputTokens: input.outputTokens,
  72. reasoningTokens: input.reasoningTokens,
  73. cacheReadTokens: input.cacheReadTokens,
  74. cacheWriteTokens: input.cacheWriteTokens,
  75. cost,
  76. })
  77. const [updated] = await tx
  78. .update(BillingTable)
  79. .set({
  80. balance: sql`${BillingTable.balance} - ${cost}`,
  81. })
  82. .where(eq(BillingTable.workspaceID, workspaceID))
  83. .returning()
  84. return updated.balance
  85. })
  86. },
  87. )
  88. export const generateCheckoutUrl = fn(
  89. z.object({
  90. successUrl: z.string(),
  91. cancelUrl: z.string(),
  92. }),
  93. async (input) => {
  94. const account = Actor.assert("user")
  95. const { successUrl, cancelUrl } = input
  96. const user = await User.fromID(account.properties.userID)
  97. const customer = await Billing.get()
  98. const session = await Billing.stripe().checkout.sessions.create({
  99. mode: "payment",
  100. line_items: [
  101. {
  102. price_data: {
  103. currency: "usd",
  104. product_data: {
  105. name: "opencode credits",
  106. },
  107. unit_amount: 2000, // $20 minimum
  108. },
  109. quantity: 1,
  110. },
  111. ],
  112. payment_intent_data: {
  113. setup_future_usage: "on_session",
  114. },
  115. ...(customer.customerID
  116. ? { customer: customer.customerID }
  117. : {
  118. customer_email: user.email,
  119. customer_creation: "always",
  120. }),
  121. metadata: {
  122. workspaceID: Actor.workspace(),
  123. },
  124. currency: "usd",
  125. payment_method_types: ["card"],
  126. success_url: successUrl,
  127. cancel_url: cancelUrl,
  128. })
  129. return session.url
  130. },
  131. )
  132. export const generatePortalUrl = fn(
  133. z.object({
  134. returnUrl: z.string(),
  135. }),
  136. async (input) => {
  137. const { returnUrl } = input
  138. const customer = await Billing.get()
  139. if (!customer?.customerID) {
  140. throw new Error("No stripe customer ID")
  141. }
  142. const session = await Billing.stripe().billingPortal.sessions.create({
  143. customer: customer.customerID,
  144. return_url: returnUrl,
  145. })
  146. return session.url
  147. },
  148. )
  149. }