billing.ts 3.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120
  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 { User } from "./user"
  8. import { Resource } from "@opencode/cloud-resource"
  9. export namespace Billing {
  10. export const stripe = () =>
  11. new Stripe(Resource.STRIPE_SECRET_KEY.value, {
  12. apiVersion: "2025-03-31.basil",
  13. })
  14. export const get = async () => {
  15. return Database.use(async (tx) =>
  16. tx
  17. .select({
  18. customerID: BillingTable.customerID,
  19. paymentMethodID: BillingTable.paymentMethodID,
  20. balance: BillingTable.balance,
  21. reload: BillingTable.reload,
  22. })
  23. .from(BillingTable)
  24. .where(eq(BillingTable.workspaceID, Actor.workspace()))
  25. .then((r) => r[0]),
  26. )
  27. }
  28. export const payments = async () => {
  29. return await Database.use((tx) =>
  30. tx
  31. .select()
  32. .from(PaymentTable)
  33. .where(eq(PaymentTable.workspaceID, Actor.workspace()))
  34. .orderBy(sql`${PaymentTable.timeCreated} DESC`)
  35. .limit(100),
  36. )
  37. }
  38. export const usages = async () => {
  39. return await Database.use((tx) =>
  40. tx
  41. .select()
  42. .from(UsageTable)
  43. .where(eq(UsageTable.workspaceID, Actor.workspace()))
  44. .orderBy(sql`${UsageTable.timeCreated} DESC`)
  45. .limit(100),
  46. )
  47. }
  48. export const generateCheckoutUrl = fn(
  49. z.object({
  50. successUrl: z.string(),
  51. cancelUrl: z.string(),
  52. }),
  53. async (input) => {
  54. const account = Actor.assert("user")
  55. const { successUrl, cancelUrl } = input
  56. const user = await User.fromID(account.properties.userID)
  57. const customer = await Billing.get()
  58. const session = await Billing.stripe().checkout.sessions.create({
  59. mode: "payment",
  60. line_items: [
  61. {
  62. price_data: {
  63. currency: "usd",
  64. product_data: {
  65. name: "opencode credits",
  66. },
  67. unit_amount: 2123, // $20 minimum + Stripe fee 4.4% + $0.30
  68. },
  69. quantity: 1,
  70. },
  71. ],
  72. payment_intent_data: {
  73. setup_future_usage: "on_session",
  74. },
  75. ...(customer.customerID
  76. ? { customer: customer.customerID }
  77. : {
  78. customer_email: user.email,
  79. customer_creation: "always",
  80. }),
  81. metadata: {
  82. workspaceID: Actor.workspace(),
  83. },
  84. currency: "usd",
  85. payment_method_types: ["card"],
  86. success_url: successUrl,
  87. cancel_url: cancelUrl,
  88. })
  89. return session.url
  90. },
  91. )
  92. export const generatePortalUrl = fn(
  93. z.object({
  94. returnUrl: z.string(),
  95. }),
  96. async (input) => {
  97. const { returnUrl } = input
  98. const customer = await Billing.get()
  99. if (!customer?.customerID) {
  100. throw new Error("No stripe customer ID")
  101. }
  102. const session = await Billing.stripe().billingPortal.sessions.create({
  103. customer: customer.customerID,
  104. return_url: returnUrl,
  105. })
  106. return session.url
  107. },
  108. )
  109. }