black-onboard.ts 5.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173
  1. import { Billing } from "../src/billing.js"
  2. import { and, Database, eq, isNull, sql } from "../src/drizzle/index.js"
  3. import { UserTable } from "../src/schema/user.sql.js"
  4. import { BillingTable, PaymentTable, SubscriptionTable } from "../src/schema/billing.sql.js"
  5. import { Identifier } from "../src/identifier.js"
  6. import { centsToMicroCents } from "../src/util/price.js"
  7. import { AuthTable } from "../src/schema/auth.sql.js"
  8. const workspaceID = process.argv[2]
  9. const email = process.argv[3]
  10. console.log(`Onboarding workspace ${workspaceID} for email ${email}`)
  11. if (!workspaceID || !email) {
  12. console.error("Usage: bun foo.ts <workspaceID> <email>")
  13. process.exit(1)
  14. }
  15. // Look up the Stripe customer by email
  16. const customers = await Billing.stripe().customers.list({ email, limit: 10, expand: ["data.subscriptions"] })
  17. if (!customers.data) {
  18. console.error(`Error: No Stripe customer found for email ${email}`)
  19. process.exit(1)
  20. }
  21. const customer = customers.data.find((c) => c.subscriptions?.data[0]?.items.data[0]?.price.unit_amount === 20000)
  22. if (!customer) {
  23. console.error(`Error: No Stripe customer found for email ${email} with $200 subscription`)
  24. process.exit(1)
  25. }
  26. const customerID = customer.id
  27. const subscription = customer.subscriptions!.data[0]
  28. const subscriptionID = subscription.id
  29. // Validate the subscription is $200
  30. const amountInCents = subscription.items.data[0]?.price.unit_amount ?? 0
  31. if (amountInCents !== 20000) {
  32. console.error(`Error: Subscription amount is $${amountInCents / 100}, expected $200`)
  33. process.exit(1)
  34. }
  35. const subscriptionData = await Billing.stripe().subscriptions.retrieve(subscription.id, { expand: ["discounts"] })
  36. const couponID =
  37. typeof subscriptionData.discounts[0] === "string"
  38. ? subscriptionData.discounts[0]
  39. : subscriptionData.discounts[0]?.coupon?.id
  40. // Check if subscription is already tied to another workspace
  41. const existingSubscription = await Database.use((tx) =>
  42. tx
  43. .select({ workspaceID: BillingTable.workspaceID })
  44. .from(BillingTable)
  45. .where(sql`JSON_EXTRACT(${BillingTable.subscription}, '$.id') = ${subscriptionID}`)
  46. .then((rows) => rows[0]),
  47. )
  48. if (existingSubscription) {
  49. console.error(
  50. `Error: Subscription ${subscriptionID} is already tied to workspace ${existingSubscription.workspaceID}`,
  51. )
  52. process.exit(1)
  53. }
  54. // Look up the workspace billing and check if it already has a customer id or subscription
  55. const billing = await Database.use((tx) =>
  56. tx
  57. .select({ customerID: BillingTable.customerID, subscriptionID: BillingTable.subscriptionID })
  58. .from(BillingTable)
  59. .where(eq(BillingTable.workspaceID, workspaceID))
  60. .then((rows) => rows[0]),
  61. )
  62. if (billing?.subscriptionID) {
  63. console.error(`Error: Workspace ${workspaceID} already has a subscription: ${billing.subscriptionID}`)
  64. process.exit(1)
  65. }
  66. if (billing?.customerID) {
  67. console.warn(
  68. `Warning: Workspace ${workspaceID} already has a customer id: ${billing.customerID}, replacing with ${customerID}`,
  69. )
  70. }
  71. // Get the latest invoice and payment from the subscription
  72. const invoices = await Billing.stripe().invoices.list({
  73. subscription: subscriptionID,
  74. limit: 1,
  75. expand: ["data.payments"],
  76. })
  77. const invoice = invoices.data[0]
  78. const invoiceID = invoice?.id
  79. const paymentID = invoice?.payments?.data[0]?.payment.payment_intent as string | undefined
  80. // Get the default payment method from the customer
  81. const paymentMethodID = (customer.invoice_settings.default_payment_method ?? subscription.default_payment_method) as
  82. | string
  83. | null
  84. const paymentMethod = paymentMethodID ? await Billing.stripe().paymentMethods.retrieve(paymentMethodID) : null
  85. const paymentMethodLast4 = paymentMethod?.card?.last4 ?? null
  86. const paymentMethodType = paymentMethod?.type ?? null
  87. // Look up the user in the workspace
  88. const users = await Database.use((tx) =>
  89. tx
  90. .select({ id: UserTable.id, email: AuthTable.subject })
  91. .from(UserTable)
  92. .innerJoin(AuthTable, and(eq(AuthTable.accountID, UserTable.accountID), eq(AuthTable.provider, "email")))
  93. .where(and(eq(UserTable.workspaceID, workspaceID), isNull(UserTable.timeDeleted))),
  94. )
  95. if (users.length === 0) {
  96. console.error(`Error: No users found in workspace ${workspaceID}`)
  97. process.exit(1)
  98. }
  99. const user = users.length === 1 ? users[0] : users.find((u) => u.email === email)
  100. if (!user) {
  101. console.error(`Error: User with email ${email} not found in workspace ${workspaceID}`)
  102. process.exit(1)
  103. }
  104. // Set workspaceID in Stripe customer metadata
  105. await Billing.stripe().customers.update(customerID, {
  106. metadata: {
  107. workspaceID,
  108. },
  109. })
  110. await Database.transaction(async (tx) => {
  111. // Set customer id, subscription id, and payment method on workspace billing
  112. await tx
  113. .update(BillingTable)
  114. .set({
  115. customerID,
  116. subscriptionID,
  117. paymentMethodID,
  118. paymentMethodLast4,
  119. paymentMethodType,
  120. subscription: {
  121. status: "subscribed",
  122. coupon: couponID,
  123. seats: 1,
  124. plan: "200",
  125. },
  126. })
  127. .where(eq(BillingTable.workspaceID, workspaceID))
  128. // Create a row in subscription table
  129. await tx.insert(SubscriptionTable).values({
  130. workspaceID,
  131. id: Identifier.create("subscription"),
  132. userID: user.id,
  133. })
  134. // Create a row in payments table
  135. await tx.insert(PaymentTable).values({
  136. workspaceID,
  137. id: Identifier.create("payment"),
  138. amount: centsToMicroCents(amountInCents),
  139. customerID,
  140. invoiceID,
  141. paymentID,
  142. enrichment: {
  143. type: "subscription",
  144. couponID,
  145. },
  146. })
  147. })
  148. console.log(`Successfully onboarded workspace ${workspaceID}`)
  149. console.log(` Customer ID: ${customerID}`)
  150. console.log(` Subscription ID: ${subscriptionID}`)
  151. console.log(
  152. ` Payment Method: ${paymentMethodID ?? "(none)"} (${paymentMethodType ?? "unknown"} ending in ${paymentMethodLast4 ?? "????"})`,
  153. )
  154. console.log(` User ID: ${user.id}`)
  155. console.log(` Invoice ID: ${invoiceID ?? "(none)"}`)
  156. console.log(` Payment ID: ${paymentID ?? "(none)"}`)