@corgelabs/corge 0.0.1 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (341) hide show
  1. package/dist/ai/index.d.ts +3 -0
  2. package/dist/ai/index.d.ts.map +1 -0
  3. package/dist/ai/index.js +3 -0
  4. package/dist/ai/index.js.map +1 -0
  5. package/dist/ai/schema.d.ts +83 -0
  6. package/dist/ai/schema.d.ts.map +1 -0
  7. package/dist/ai/schema.js +19 -0
  8. package/dist/ai/schema.js.map +1 -0
  9. package/dist/ai/service.d.ts +14 -0
  10. package/dist/ai/service.d.ts.map +1 -0
  11. package/dist/ai/service.js +266 -0
  12. package/dist/ai/service.js.map +1 -0
  13. package/dist/ai/utils.d.ts +10 -0
  14. package/dist/ai/utils.d.ts.map +1 -0
  15. package/dist/ai/utils.js +27 -0
  16. package/dist/ai/utils.js.map +1 -0
  17. package/dist/attachments/index.d.ts +4 -0
  18. package/dist/attachments/index.d.ts.map +1 -0
  19. package/dist/attachments/index.js +4 -0
  20. package/dist/attachments/index.js.map +1 -0
  21. package/dist/attachments/routes.d.ts +75 -0
  22. package/dist/attachments/routes.d.ts.map +1 -0
  23. package/dist/attachments/routes.js +18 -0
  24. package/dist/attachments/routes.js.map +1 -0
  25. package/dist/attachments/schema.d.ts +61 -0
  26. package/dist/attachments/schema.d.ts.map +1 -0
  27. package/dist/attachments/schema.js +30 -0
  28. package/dist/attachments/schema.js.map +1 -0
  29. package/dist/attachments/service.d.ts +27 -0
  30. package/dist/attachments/service.d.ts.map +1 -0
  31. package/dist/attachments/service.js +224 -0
  32. package/dist/attachments/service.js.map +1 -0
  33. package/dist/attachments/utils.d.ts +9 -0
  34. package/dist/attachments/utils.d.ts.map +1 -0
  35. package/dist/attachments/utils.js +80 -0
  36. package/dist/attachments/utils.js.map +1 -0
  37. package/dist/auth/index.d.ts +4 -0
  38. package/dist/auth/index.d.ts.map +1 -0
  39. package/dist/auth/index.js +4 -0
  40. package/dist/auth/index.js.map +1 -0
  41. package/dist/auth/oidc.d.ts +25 -0
  42. package/dist/auth/oidc.d.ts.map +1 -0
  43. package/dist/auth/oidc.js +298 -0
  44. package/dist/auth/oidc.js.map +1 -0
  45. package/dist/auth/recovery.d.ts +41 -0
  46. package/dist/auth/recovery.d.ts.map +1 -0
  47. package/dist/auth/recovery.js +250 -0
  48. package/dist/auth/recovery.js.map +1 -0
  49. package/dist/auth/routes.d.ts +226 -0
  50. package/dist/auth/routes.d.ts.map +1 -0
  51. package/dist/auth/routes.js +96 -0
  52. package/dist/auth/routes.js.map +1 -0
  53. package/dist/auth/schema.d.ts +158 -0
  54. package/dist/auth/schema.d.ts.map +1 -0
  55. package/dist/auth/schema.js +82 -0
  56. package/dist/auth/schema.js.map +1 -0
  57. package/dist/auth/service.d.ts +96 -0
  58. package/dist/auth/service.d.ts.map +1 -0
  59. package/dist/auth/service.js +129 -0
  60. package/dist/auth/service.js.map +1 -0
  61. package/dist/auth/session.d.ts +51 -0
  62. package/dist/auth/session.d.ts.map +1 -0
  63. package/dist/auth/session.js +344 -0
  64. package/dist/auth/session.js.map +1 -0
  65. package/dist/auth/utils.d.ts +17 -0
  66. package/dist/auth/utils.d.ts.map +1 -0
  67. package/dist/auth/utils.js +96 -0
  68. package/dist/auth/utils.js.map +1 -0
  69. package/dist/bin/corge.js +547 -0
  70. package/dist/chat/context.d.ts +10 -0
  71. package/dist/chat/context.d.ts.map +1 -0
  72. package/dist/chat/context.js +221 -0
  73. package/dist/chat/context.js.map +1 -0
  74. package/dist/chat/index.d.ts +4 -0
  75. package/dist/chat/index.d.ts.map +1 -0
  76. package/dist/chat/index.js +4 -0
  77. package/dist/chat/index.js.map +1 -0
  78. package/dist/chat/routes.d.ts +160 -0
  79. package/dist/chat/routes.d.ts.map +1 -0
  80. package/dist/chat/routes.js +210 -0
  81. package/dist/chat/routes.js.map +1 -0
  82. package/dist/chat/schema.d.ts +285 -0
  83. package/dist/chat/schema.d.ts.map +1 -0
  84. package/dist/chat/schema.js +74 -0
  85. package/dist/chat/schema.js.map +1 -0
  86. package/dist/chat/service.d.ts +64 -0
  87. package/dist/chat/service.d.ts.map +1 -0
  88. package/dist/chat/service.js +279 -0
  89. package/dist/chat/service.js.map +1 -0
  90. package/dist/chat/stream.d.ts +53 -0
  91. package/dist/chat/stream.d.ts.map +1 -0
  92. package/dist/chat/stream.js +339 -0
  93. package/dist/chat/stream.js.map +1 -0
  94. package/dist/chat/utils.d.ts +61 -0
  95. package/dist/chat/utils.d.ts.map +1 -0
  96. package/dist/chat/utils.js +285 -0
  97. package/dist/chat/utils.js.map +1 -0
  98. package/dist/cli/builder.d.ts +6 -0
  99. package/dist/cli/builder.d.ts.map +1 -0
  100. package/dist/cli/builder.js +91 -0
  101. package/dist/cli/builder.js.map +1 -0
  102. package/dist/cli/deployer.d.ts +2 -0
  103. package/dist/cli/deployer.d.ts.map +1 -0
  104. package/dist/cli/deployer.js +152 -0
  105. package/dist/cli/deployer.js.map +1 -0
  106. package/dist/cli/dev-server.d.ts +2 -0
  107. package/dist/cli/dev-server.d.ts.map +1 -0
  108. package/dist/cli/dev-server.js +70 -0
  109. package/dist/cli/dev-server.js.map +1 -0
  110. package/dist/cli/env.d.ts +4 -0
  111. package/dist/cli/env.d.ts.map +1 -0
  112. package/dist/cli/env.js +68 -0
  113. package/dist/cli/env.js.map +1 -0
  114. package/dist/cli/init.d.ts +6 -0
  115. package/dist/cli/init.d.ts.map +1 -0
  116. package/dist/cli/init.js +103 -0
  117. package/dist/cli/init.js.map +1 -0
  118. package/dist/config.d.ts +262 -0
  119. package/dist/config.d.ts.map +1 -0
  120. package/dist/config.js +212 -0
  121. package/dist/config.js.map +1 -0
  122. package/dist/db/index.d.ts +3 -0
  123. package/dist/db/index.d.ts.map +1 -0
  124. package/dist/db/index.js +3 -0
  125. package/dist/db/index.js.map +1 -0
  126. package/dist/db/schema.d.ts +410 -0
  127. package/dist/db/schema.d.ts.map +1 -0
  128. package/dist/db/schema.js +294 -0
  129. package/dist/db/schema.js.map +1 -0
  130. package/dist/db/service.d.ts +42 -0
  131. package/dist/db/service.d.ts.map +1 -0
  132. package/dist/db/service.js +372 -0
  133. package/dist/db/service.js.map +1 -0
  134. package/dist/db/utils.d.ts +25 -0
  135. package/dist/db/utils.d.ts.map +1 -0
  136. package/dist/db/utils.js +188 -0
  137. package/dist/db/utils.js.map +1 -0
  138. package/dist/emails/index.d.ts +4 -0
  139. package/dist/emails/index.d.ts.map +1 -0
  140. package/dist/emails/index.js +4 -0
  141. package/dist/emails/index.js.map +1 -0
  142. package/dist/emails/routes.d.ts +191 -0
  143. package/dist/emails/routes.d.ts.map +1 -0
  144. package/dist/emails/routes.js +39 -0
  145. package/dist/emails/routes.js.map +1 -0
  146. package/dist/emails/schema.d.ts +237 -0
  147. package/dist/emails/schema.d.ts.map +1 -0
  148. package/dist/emails/schema.js +98 -0
  149. package/dist/emails/schema.js.map +1 -0
  150. package/dist/emails/service.d.ts +22 -0
  151. package/dist/emails/service.d.ts.map +1 -0
  152. package/dist/emails/service.js +337 -0
  153. package/dist/emails/service.js.map +1 -0
  154. package/dist/emails/utils.d.ts +49 -0
  155. package/dist/emails/utils.d.ts.map +1 -0
  156. package/dist/emails/utils.js +341 -0
  157. package/dist/emails/utils.js.map +1 -0
  158. package/dist/files/index.d.ts +4 -0
  159. package/dist/files/index.d.ts.map +1 -0
  160. package/dist/files/index.js +4 -0
  161. package/dist/files/index.js.map +1 -0
  162. package/dist/files/routes.d.ts +72 -0
  163. package/dist/files/routes.d.ts.map +1 -0
  164. package/dist/files/routes.js +38 -0
  165. package/dist/files/routes.js.map +1 -0
  166. package/dist/files/schema.d.ts +57 -0
  167. package/dist/files/schema.d.ts.map +1 -0
  168. package/dist/files/schema.js +39 -0
  169. package/dist/files/schema.js.map +1 -0
  170. package/dist/files/service.d.ts +27 -0
  171. package/dist/files/service.d.ts.map +1 -0
  172. package/dist/files/service.js +175 -0
  173. package/dist/files/service.js.map +1 -0
  174. package/dist/files/utils.d.ts +3 -0
  175. package/dist/files/utils.d.ts.map +1 -0
  176. package/dist/files/utils.js +63 -0
  177. package/dist/files/utils.js.map +1 -0
  178. package/dist/index.d.ts +875 -0
  179. package/dist/index.d.ts.map +1 -0
  180. package/dist/index.js +9919 -0
  181. package/dist/index.js.map +7 -0
  182. package/dist/payments/billing.d.ts +29 -0
  183. package/dist/payments/billing.d.ts.map +1 -0
  184. package/dist/payments/billing.js +378 -0
  185. package/dist/payments/billing.js.map +1 -0
  186. package/dist/payments/index.d.ts +4 -0
  187. package/dist/payments/index.d.ts.map +1 -0
  188. package/dist/payments/index.js +4 -0
  189. package/dist/payments/index.js.map +1 -0
  190. package/dist/payments/routes.d.ts +340 -0
  191. package/dist/payments/routes.d.ts.map +1 -0
  192. package/dist/payments/routes.js +56 -0
  193. package/dist/payments/routes.js.map +1 -0
  194. package/dist/payments/schema.d.ts +412 -0
  195. package/dist/payments/schema.d.ts.map +1 -0
  196. package/dist/payments/schema.js +260 -0
  197. package/dist/payments/schema.js.map +1 -0
  198. package/dist/payments/service.d.ts +31 -0
  199. package/dist/payments/service.d.ts.map +1 -0
  200. package/dist/payments/service.js +322 -0
  201. package/dist/payments/service.js.map +1 -0
  202. package/dist/payments/utils.d.ts +61 -0
  203. package/dist/payments/utils.d.ts.map +1 -0
  204. package/dist/payments/utils.js +369 -0
  205. package/dist/payments/utils.js.map +1 -0
  206. package/dist/router/guard.d.ts +19 -0
  207. package/dist/router/guard.d.ts.map +1 -0
  208. package/dist/router/guard.js +243 -0
  209. package/dist/router/guard.js.map +1 -0
  210. package/dist/router/index.d.ts +3 -0
  211. package/dist/router/index.d.ts.map +1 -0
  212. package/dist/router/index.js +3 -0
  213. package/dist/router/index.js.map +1 -0
  214. package/dist/router/schema.d.ts +103 -0
  215. package/dist/router/schema.d.ts.map +1 -0
  216. package/dist/router/schema.js +282 -0
  217. package/dist/router/schema.js.map +1 -0
  218. package/dist/router/service.d.ts +25 -0
  219. package/dist/router/service.d.ts.map +1 -0
  220. package/dist/router/service.js +339 -0
  221. package/dist/router/service.js.map +1 -0
  222. package/dist/tasks/index.d.ts +4 -0
  223. package/dist/tasks/index.d.ts.map +1 -0
  224. package/dist/tasks/index.js +4 -0
  225. package/dist/tasks/index.js.map +1 -0
  226. package/dist/tasks/routes.d.ts +54 -0
  227. package/dist/tasks/routes.d.ts.map +1 -0
  228. package/dist/tasks/routes.js +195 -0
  229. package/dist/tasks/routes.js.map +1 -0
  230. package/dist/tasks/schema.d.ts +261 -0
  231. package/dist/tasks/schema.d.ts.map +1 -0
  232. package/dist/tasks/schema.js +35 -0
  233. package/dist/tasks/schema.js.map +1 -0
  234. package/dist/tasks/service.d.ts +32 -0
  235. package/dist/tasks/service.d.ts.map +1 -0
  236. package/dist/tasks/service.js +201 -0
  237. package/dist/tasks/service.js.map +1 -0
  238. package/dist/tasks/steps.d.ts +18 -0
  239. package/dist/tasks/steps.d.ts.map +1 -0
  240. package/dist/tasks/steps.js +365 -0
  241. package/dist/tasks/steps.js.map +1 -0
  242. package/dist/tasks/utils.d.ts +75 -0
  243. package/dist/tasks/utils.d.ts.map +1 -0
  244. package/dist/tasks/utils.js +311 -0
  245. package/dist/tasks/utils.js.map +1 -0
  246. package/dist/tasks/watcher.d.ts +13 -0
  247. package/dist/tasks/watcher.d.ts.map +1 -0
  248. package/dist/tasks/watcher.js +255 -0
  249. package/dist/tasks/watcher.js.map +1 -0
  250. package/dist/tasks/worker.d.ts +28 -0
  251. package/dist/tasks/worker.d.ts.map +1 -0
  252. package/dist/tasks/worker.js +326 -0
  253. package/dist/tasks/worker.js.map +1 -0
  254. package/dist/utils/index.d.ts +3 -0
  255. package/dist/utils/index.d.ts.map +1 -0
  256. package/dist/utils/index.js +3 -0
  257. package/dist/utils/index.js.map +1 -0
  258. package/dist/utils/schema.d.ts +79 -0
  259. package/dist/utils/schema.d.ts.map +1 -0
  260. package/dist/utils/schema.js +41 -0
  261. package/dist/utils/schema.js.map +1 -0
  262. package/dist/utils/service.d.ts +32 -0
  263. package/dist/utils/service.d.ts.map +1 -0
  264. package/dist/utils/service.js +384 -0
  265. package/dist/utils/service.js.map +1 -0
  266. package/docs/SDK_SPEC.md +1645 -0
  267. package/infra/firestore.base.indexes.json +138 -0
  268. package/infra/firestore.base.rules +152 -0
  269. package/infra/storage.base.rules +8 -0
  270. package/package.json +59 -13
  271. package/src/ai/index.ts +2 -0
  272. package/src/ai/schema.ts +64 -0
  273. package/src/ai/service.ts +317 -0
  274. package/src/ai/utils.ts +38 -0
  275. package/src/attachments/index.ts +3 -0
  276. package/src/attachments/routes.ts +28 -0
  277. package/src/attachments/schema.ts +45 -0
  278. package/src/attachments/service.ts +315 -0
  279. package/src/attachments/utils.ts +115 -0
  280. package/src/auth/index.ts +3 -0
  281. package/src/auth/oidc.ts +419 -0
  282. package/src/auth/recovery.ts +365 -0
  283. package/src/auth/routes.ts +142 -0
  284. package/src/auth/schema.ts +181 -0
  285. package/src/auth/service.ts +183 -0
  286. package/src/auth/session.ts +414 -0
  287. package/src/auth/utils.ts +106 -0
  288. package/src/chat/context.ts +310 -0
  289. package/src/chat/index.ts +3 -0
  290. package/src/chat/routes.ts +264 -0
  291. package/src/chat/schema.ts +295 -0
  292. package/src/chat/service.ts +360 -0
  293. package/src/chat/stream.ts +481 -0
  294. package/src/chat/utils.ts +370 -0
  295. package/src/cli/builder.ts +93 -0
  296. package/src/cli/deployer.ts +184 -0
  297. package/src/cli/dev-server.ts +77 -0
  298. package/src/cli/env.ts +89 -0
  299. package/src/cli/init.ts +125 -0
  300. package/src/config.ts +447 -0
  301. package/src/db/index.ts +2 -0
  302. package/src/db/schema.ts +409 -0
  303. package/src/db/service.ts +477 -0
  304. package/src/db/utils.ts +250 -0
  305. package/src/emails/index.ts +3 -0
  306. package/src/emails/routes.ts +64 -0
  307. package/src/emails/schema.ts +239 -0
  308. package/src/emails/service.ts +488 -0
  309. package/src/emails/utils.ts +449 -0
  310. package/src/files/index.ts +3 -0
  311. package/src/files/routes.ts +44 -0
  312. package/src/files/schema.ts +76 -0
  313. package/src/files/service.ts +238 -0
  314. package/src/files/utils.ts +77 -0
  315. package/src/index.ts +425 -0
  316. package/src/payments/billing.ts +488 -0
  317. package/src/payments/index.ts +3 -0
  318. package/src/payments/routes.ts +92 -0
  319. package/src/payments/schema.ts +401 -0
  320. package/src/payments/service.ts +445 -0
  321. package/src/payments/utils.ts +472 -0
  322. package/src/router/guard.ts +319 -0
  323. package/src/router/index.ts +2 -0
  324. package/src/router/schema.ts +458 -0
  325. package/src/router/service.ts +453 -0
  326. package/src/tasks/index.ts +11 -0
  327. package/src/tasks/routes.ts +230 -0
  328. package/src/tasks/schema.ts +310 -0
  329. package/src/tasks/service.ts +254 -0
  330. package/src/tasks/steps.ts +472 -0
  331. package/src/tasks/utils.ts +445 -0
  332. package/src/tasks/watcher.ts +307 -0
  333. package/src/tasks/worker.ts +461 -0
  334. package/src/utils/index.ts +2 -0
  335. package/src/utils/schema.ts +69 -0
  336. package/src/utils/service.ts +461 -0
  337. package/tests/e2e.test.ts +2531 -0
  338. package/tsconfig.base.json +12 -0
  339. package/README.md +0 -2
  340. package/index.d.ts +0 -1
  341. package/index.js +0 -1
@@ -0,0 +1,488 @@
1
+ import type { EntryId, SelectModel } from '../db'
2
+ import type { Core } from '../index'
3
+ import {
4
+ cancelSubscriptionInput, changeTierInput, createCheckoutInput, createInvoiceInput, createPaymentLinkInput, directChargeInput,
5
+ getPlanDefinition, getProductDefinition, subscribeInput, type CancelSubscriptionInput, type ChangeTierInput, type ChargeResult,
6
+ type CheckoutSessionResult, type CreateCheckoutInput, type CreateInvoiceInput, type CreatePaymentLinkInput, type DirectChargeInput,
7
+ type InvoiceResult, type PaymentCallOptions, type PaymentLinkResult, type PaymentProvider, type SubscribeInput, type SubscribeResult, type SubscriptionRecord
8
+ } from './schema'
9
+ import {
10
+ buildDodoCartOrCustom, buildStripeLineItem, currentPeriodKey, idempotencyPaymentKey, paymentUsageDocId,
11
+ requestDodoApi, requestStripeApi, resolveEnvScopedValue, resolvePurchaseItem, type ResolvedPurchaseItem
12
+ } from './utils'
13
+
14
+ export class PaymentsBillingEngine {
15
+ readonly #core: Core
16
+
17
+ constructor(coreClient: Core) {
18
+ this.#core = coreClient
19
+ }
20
+
21
+ resolveProvider = (override?: PaymentProvider): PaymentProvider => {
22
+ if (override) return override
23
+ const preferred = this.#core.config.payments.provider
24
+ const isDev = Boolean(this.#core.env.isDev)
25
+ const env = this.#core.env
26
+ const hasStripe = Boolean((isDev ? env.STRIPE_SANDBOX_API_KEY : env.STRIPE_API_KEY) || env.STRIPE_API_KEY || env.STRIPE_SANDBOX_API_KEY)
27
+ const hasDodo = Boolean((isDev ? env.DODO_SANDBOX_API_KEY : env.DODO_API_KEY) || env.DODO_API_KEY || env.DODO_SANDBOX_API_KEY)
28
+ if (preferred === 'dodo' && !hasDodo && hasStripe) return 'stripe'
29
+ if (preferred === 'stripe' && !hasStripe && hasDodo) return 'dodo'
30
+ return preferred
31
+ }
32
+
33
+ getCreds = (provider: PaymentProvider) => {
34
+ const isDev = Boolean(this.#core.env.isDev)
35
+ const cfg = this.#core.config.payments
36
+ const env = this.#core.env
37
+ if (provider === 'stripe') {
38
+ const apiKey = (isDev && env.STRIPE_SANDBOX_API_KEY ? env.STRIPE_SANDBOX_API_KEY : env.STRIPE_API_KEY || env.STRIPE_SANDBOX_API_KEY).trim()
39
+ const webhookSecret = (isDev && env.STRIPE_SANDBOX_WEBHOOK_SECRET ? env.STRIPE_SANDBOX_WEBHOOK_SECRET : env.STRIPE_WEBHOOK_SECRET || env.STRIPE_SANDBOX_WEBHOOK_SECRET).trim()
40
+ return { apiKey, webhookSecret, baseUrl: cfg.stripe.baseUrl, isSyntheticDev: isDev && !apiKey }
41
+ }
42
+ const apiKey = (isDev && env.DODO_SANDBOX_API_KEY ? env.DODO_SANDBOX_API_KEY : env.DODO_API_KEY || env.DODO_SANDBOX_API_KEY).trim()
43
+ const webhookSecret = (isDev && env.DODO_SANDBOX_WEBHOOK_SECRET ? env.DODO_SANDBOX_WEBHOOK_SECRET : env.DODO_WEBHOOK_SECRET || env.DODO_SANDBOX_WEBHOOK_SECRET).trim()
44
+ return { apiKey, webhookSecret, baseUrl: isDev ? cfg.dodo.testBaseUrl : cfg.dodo.liveBaseUrl, isSyntheticDev: isDev && !apiKey }
45
+ }
46
+
47
+ #withBreaker = async <T>(cost: number, fn: () => Promise<T>): Promise<T> => {
48
+ await this.#core.internals.resilience.acquireLimit('payments', cost)
49
+ return this.#core.internals.resilience.circuitBreaker('payments', fn, undefined, { isDev: this.#core.env.isDev })
50
+ }
51
+
52
+ stripeCall = <T = Record<string, any>>(method: string, path: string, form?: Record<string, unknown>, idempotencyKey?: string): Promise<T> => {
53
+ const { baseUrl, apiKey } = this.getCreds('stripe')
54
+ return this.#withBreaker(1, () => requestStripeApi<T>(baseUrl, apiKey, this.#core.config.payments.stripe.apiVersion, method, path, form, idempotencyKey))
55
+ }
56
+
57
+ dodoCall = <T = Record<string, any>>(method: string, path: string, body?: unknown, idempotencyKey?: string): Promise<T> => {
58
+ const { baseUrl, apiKey } = this.getCreds('dodo')
59
+ return this.#withBreaker(1, () => requestDodoApi<T>(baseUrl, apiKey, method, path, body, idempotencyKey))
60
+ }
61
+
62
+ #resolveItem = (rawInput: Parameters<typeof resolvePurchaseItem>[0], userId: string): ResolvedPurchaseItem => {
63
+ const res = resolvePurchaseItem(rawInput, userId, Boolean(this.#core.env.isDev), this.#core.config.payments.defaultCurrency)
64
+ if ('error' in res) return this.#core.errors.httpError(400, res.error, 'INVALID_PAYMENT_ITEM')
65
+ return res
66
+ }
67
+
68
+ ensureCustomer = async (userId: EntryId, provider: PaymentProvider): Promise<{ doc: SelectModel<'payment_customer'>; providerCustomerId: string }> =>
69
+ this.#core.internals.resilience.singleflight(
70
+ `pay_cust:${provider}:${userId}`,
71
+ async () => {
72
+ const defaultPlanId = this.#core.config.payments.defaultPlanId
73
+ let doc = await this.#core.db.read('payment_customer', userId, false, { bypassRbac: true })
74
+ if (!doc) {
75
+ doc = await this.#core.db.create(
76
+ 'payment_customer',
77
+ { id: userId, userId, planId: defaultPlanId, subscriptionStatus: 'none', cancelAtPeriodEnd: false, bonusAiTokens: 0, credits: 0, bonusMetrics: {} },
78
+ { bypassRbac: true }
79
+ )
80
+ }
81
+ const existingCusId = provider === 'stripe' ? doc.stripeCustomerId : doc.dodoCustomerId
82
+ if (existingCusId) return { doc, providerCustomerId: existingCusId }
83
+
84
+ let providerCustomerId = `cus_dev_${provider}_${userId}`
85
+ if (!this.getCreds(provider).isSyntheticDev) {
86
+ const custIdemKey = `cust_${provider}_${userId}`
87
+ const created =
88
+ provider === 'stripe'
89
+ ? await this.stripeCall('POST', '/customers', { ...(doc.email ? { email: doc.email } : {}), ...(doc.name ? { name: doc.name } : {}), metadata: { userId } }, custIdemKey)
90
+ : await this.dodoCall('POST', '/customers', { email: doc.email ?? `${userId}@customer.local`, name: doc.name ?? userId, metadata: { userId } }, custIdemKey)
91
+ providerCustomerId = String(created.id ?? created.customer_id ?? providerCustomerId)
92
+ }
93
+ const patch = provider === 'stripe' ? { stripeCustomerId: providerCustomerId, activeProvider: provider } : { dodoCustomerId: providerCustomerId, activeProvider: provider }
94
+ await this.#core.db.update('payment_customer', userId, patch, { bypassRbac: true })
95
+ return { doc: { ...doc, ...patch }, providerCustomerId }
96
+ },
97
+ { scope: 'global' }
98
+ )
99
+
100
+ applyProductGrants = async (userId: EntryId, productId?: string, grantKey?: string): Promise<void> => {
101
+ const prod = productId ? getProductDefinition(productId) : undefined
102
+ if (!prod?.grants) return
103
+ const db = this.#core.db.firestore()
104
+ const custRef = db.collection('payment_customer').doc(userId)
105
+ const grantRef = grantKey ? db.collection('_idempotency').doc(`pay_grant_${userId}_${grantKey.replace(/[^A-Za-z0-9_-]/g, '_').slice(0, 96)}`) : undefined
106
+ await this.#core.internals.resilience.acquireLimit('firestore')
107
+ await db.runTransaction(async (tx) => {
108
+ const [custSnap, grantSnap] = await Promise.all([tx.get(custRef), grantRef ? tx.get(grantRef) : Promise.resolve(undefined)])
109
+ if (grantSnap?.exists) return
110
+ const data = custSnap.data() ?? {}
111
+ const bonusMetrics: Record<string, number> = { ...(data.bonusMetrics ?? {}) }
112
+ for (const [m, delta] of Object.entries(prod.grants?.metrics ?? {})) bonusMetrics[m] = (bonusMetrics[m] ?? 0) + delta
113
+ const now = new Date()
114
+ if (grantRef) tx.set(grantRef, { key: grantRef.id, userId, productId, createdAt: now, expiresAt: new Date(now.getTime() + 86_400_000) })
115
+ tx.set(
116
+ custRef,
117
+ {
118
+ id: userId,
119
+ userId,
120
+ planId: data.planId ?? this.#core.config.payments.defaultPlanId,
121
+ subscriptionStatus: data.subscriptionStatus ?? 'none',
122
+ cancelAtPeriodEnd: Boolean(data.cancelAtPeriodEnd),
123
+ bonusAiTokens: Number(data.bonusAiTokens ?? 0) + (prod.grants?.aiTokens ?? 0),
124
+ credits: Number(data.credits ?? 0) + (prod.grants?.credits ?? 0),
125
+ bonusMetrics,
126
+ deleted: false,
127
+ createdAt: data.createdAt ?? now,
128
+ updatedAt: now
129
+ },
130
+ { merge: true }
131
+ )
132
+ })
133
+ }
134
+
135
+ createCheckout = async (rawInput: CreateCheckoutInput, opts?: PaymentCallOptions): Promise<CheckoutSessionResult> => {
136
+ const parsed = createCheckoutInput.parse(rawInput)
137
+ const userId = opts?.userId ?? this.#core.userId
138
+ const provider = this.resolveProvider(opts?.provider)
139
+ const isDev = Boolean(this.#core.env.isDev)
140
+ const item = this.#resolveItem(parsed, userId)
141
+ const { providerCustomerId } = await this.ensureCustomer(userId, provider)
142
+ const cfgUrls = this.#core.config.payments.urls
143
+ const successUrl = parsed.successUrl ?? resolveEnvScopedValue(cfgUrls.successUrl, isDev) ?? 'https://localhost/success'
144
+ const cancelUrl = parsed.cancelUrl ?? resolveEnvScopedValue(cfgUrls.cancelUrl, isDev) ?? 'https://localhost/cancel'
145
+ const idemKey = idempotencyPaymentKey('chk', parsed.idempotencyKey ?? opts?.idempotencyKey)
146
+
147
+ if (this.getCreds(provider).isSyntheticDev) {
148
+ const checkoutId = `chk_dev_${crypto.randomUUID().replace(/-/g, '').slice(0, 16)}`
149
+ return { checkoutId, url: `${successUrl}?session_id=${checkoutId}`, provider }
150
+ }
151
+ if (provider === 'stripe') {
152
+ const res = await this.stripeCall(
153
+ 'POST',
154
+ '/checkout/sessions',
155
+ {
156
+ mode: item.kind === 'plan' ? 'subscription' : 'payment',
157
+ customer: providerCustomerId,
158
+ success_url: successUrl,
159
+ cancel_url: cancelUrl,
160
+ line_items: [buildStripeLineItem(item)],
161
+ metadata: item.metadata
162
+ },
163
+ idemKey
164
+ )
165
+ return {
166
+ checkoutId: String(res.id ?? `cs_${Date.now()}`),
167
+ url: String(res.url ?? `${successUrl}?session_id=${res.id}`),
168
+ provider: 'stripe',
169
+ ...(typeof res.expires_at === 'number' ? { expiresAt: new Date(res.expires_at * 1000).toISOString() } : {})
170
+ }
171
+ }
172
+ const res = await this.dodoCall(
173
+ 'POST',
174
+ '/checkouts',
175
+ { customer: { customer_id: providerCustomerId }, return_url: successUrl, ...buildDodoCartOrCustom(item), metadata: item.metadata },
176
+ idemKey
177
+ )
178
+ const checkoutId = String(res.checkout_id ?? res.session_id ?? res.id ?? `chk_${Date.now()}`)
179
+ return { checkoutId, url: String(res.checkout_url ?? res.payment_link ?? res.url ?? `${successUrl}?session_id=${checkoutId}`), provider: 'dodo' }
180
+ }
181
+
182
+ createPaymentLink = async (rawInput: CreatePaymentLinkInput, opts?: PaymentCallOptions): Promise<PaymentLinkResult> => {
183
+ const parsed = createPaymentLinkInput.parse(rawInput)
184
+ const userId = opts?.userId ?? this.#core.userId
185
+ const provider = this.resolveProvider(opts?.provider)
186
+ const isDev = Boolean(this.#core.env.isDev)
187
+ const item = this.#resolveItem(parsed, userId)
188
+ const { providerCustomerId } = await this.ensureCustomer(userId, provider)
189
+ const returnUrl = parsed.returnUrl ?? resolveEnvScopedValue(this.#core.config.payments.urls.returnUrl, isDev) ?? 'https://localhost/billing'
190
+ const idemKey = idempotencyPaymentKey('pl', parsed.idempotencyKey ?? opts?.idempotencyKey)
191
+
192
+ if (this.getCreds(provider).isSyntheticDev) {
193
+ const paymentLinkId = `pl_dev_${crypto.randomUUID().replace(/-/g, '').slice(0, 16)}`
194
+ return { paymentLinkId, url: `${returnUrl}?payment_link=${paymentLinkId}`, provider }
195
+ }
196
+ if (provider === 'stripe') {
197
+ const res = await this.stripeCall(
198
+ 'POST',
199
+ '/payment_links',
200
+ { line_items: [buildStripeLineItem(item)], after_completion: { type: 'redirect', redirect: { url: returnUrl } }, metadata: item.metadata },
201
+ idemKey
202
+ )
203
+ return { paymentLinkId: String(res.id ?? `plink_${Date.now()}`), url: String(res.url ?? `${returnUrl}?payment_link=${res.id}`), provider: 'stripe' }
204
+ }
205
+ const res = await this.dodoCall(
206
+ 'POST',
207
+ '/payments',
208
+ { payment_link: true, customer: { customer_id: providerCustomerId }, return_url: returnUrl, ...buildDodoCartOrCustom(item), metadata: item.metadata },
209
+ idemKey
210
+ )
211
+ const paymentLinkId = String(res.payment_id ?? res.id ?? `plink_${Date.now()}`)
212
+ return { paymentLinkId, url: String(res.payment_link ?? res.url ?? `${returnUrl}?payment_link=${paymentLinkId}`), provider: 'dodo' }
213
+ }
214
+
215
+ createInvoice = async (rawInput: CreateInvoiceInput, opts?: PaymentCallOptions): Promise<InvoiceResult> => {
216
+ const parsed = createInvoiceInput.parse(rawInput)
217
+ const userId = opts?.userId ?? this.#core.userId
218
+ const provider = this.resolveProvider(opts?.provider)
219
+ const item = this.#resolveItem(parsed, userId)
220
+ const totalAmount = item.amount * item.quantity
221
+ const { providerCustomerId } = await this.ensureCustomer(userId, provider)
222
+ const idemKey = idempotencyPaymentKey('inv', parsed.idempotencyKey ?? opts?.idempotencyKey)
223
+
224
+ if (this.getCreds(provider).isSyntheticDev) {
225
+ const invoiceId = `inv_dev_${crypto.randomUUID().replace(/-/g, '').slice(0, 16)}`
226
+ return {
227
+ invoiceId,
228
+ status: 'open',
229
+ amount: totalAmount,
230
+ currency: item.currency,
231
+ hostedInvoiceUrl: `https://localhost/invoices/${invoiceId}`,
232
+ pdfUrl: `https://localhost/invoices/${invoiceId}.pdf`,
233
+ provider
234
+ }
235
+ }
236
+ if (provider === 'stripe') {
237
+ await this.stripeCall('POST', '/invoiceitems', { customer: providerCustomerId, amount: totalAmount, currency: item.currency, description: item.name })
238
+ const draft = await this.stripeCall(
239
+ 'POST',
240
+ '/invoices',
241
+ { customer: providerCustomerId, collection_method: 'send_invoice', days_until_due: parsed.daysUntilDue, metadata: item.metadata },
242
+ idemKey
243
+ )
244
+ const invId = String(draft.id ?? `in_${Date.now()}`)
245
+ const finalized = await this.stripeCall('POST', `/invoices/${invId}/finalize`)
246
+ return {
247
+ invoiceId: invId,
248
+ status: 'open',
249
+ amount: totalAmount,
250
+ currency: item.currency,
251
+ hostedInvoiceUrl: String(finalized.hosted_invoice_url ?? `https://invoice.stripe.com/i/${invId}`),
252
+ ...(finalized.invoice_pdf ? { pdfUrl: String(finalized.invoice_pdf) } : {}),
253
+ provider: 'stripe'
254
+ }
255
+ }
256
+ const { baseUrl } = this.getCreds('dodo')
257
+ const res = await this.dodoCall(
258
+ 'POST',
259
+ '/payments',
260
+ {
261
+ payment_link: true,
262
+ generate_invoice: true,
263
+ customer: { customer_id: providerCustomerId },
264
+ billing: { amount: totalAmount, currency: item.currency, name: item.name, ...(item.description ? { description: item.description } : {}) },
265
+ metadata: item.metadata
266
+ },
267
+ idemKey
268
+ )
269
+ const invoiceId = String(res.payment_id ?? res.invoice_id ?? res.id ?? `inv_${Date.now()}`)
270
+ return {
271
+ invoiceId,
272
+ status: 'open',
273
+ amount: totalAmount,
274
+ currency: item.currency,
275
+ hostedInvoiceUrl: String(res.payment_link ?? res.hosted_invoice_url ?? `${baseUrl}/invoices/${invoiceId}`),
276
+ pdfUrl: String(res.pdf_url ?? `${baseUrl.replace(/\/+$/, '')}/invoices/payments/${invoiceId}`),
277
+ provider: 'dodo'
278
+ }
279
+ }
280
+
281
+ charge = async (rawInput: DirectChargeInput, opts?: PaymentCallOptions): Promise<ChargeResult> => {
282
+ const parsed = directChargeInput.parse(rawInput)
283
+ const userId = opts?.userId ?? this.#core.userId
284
+ const provider = this.resolveProvider(opts?.provider)
285
+ const item = this.#resolveItem(parsed, userId)
286
+ const totalAmount = item.amount * item.quantity
287
+ const { providerCustomerId } = await this.ensureCustomer(userId, provider)
288
+ const idemKey = idempotencyPaymentKey('chg', parsed.idempotencyKey ?? opts?.idempotencyKey)
289
+
290
+ if (this.getCreds(provider).isSyntheticDev) {
291
+ const chargeId = idemKey ? `chg_dev_${idemKey.slice(4, 20)}` : `chg_dev_${crypto.randomUUID().replace(/-/g, '').slice(0, 16)}`
292
+ if (item.kind === 'product') await this.applyProductGrants(userId, item.id, chargeId)
293
+ return { chargeId, status: 'succeeded', amount: totalAmount, currency: item.currency, provider }
294
+ }
295
+ if (provider === 'stripe') {
296
+ const res = await this.stripeCall(
297
+ 'POST',
298
+ '/payment_intents',
299
+ {
300
+ customer: providerCustomerId,
301
+ amount: totalAmount,
302
+ currency: item.currency,
303
+ description: item.name,
304
+ ...(parsed.paymentMethodId ? { payment_method: parsed.paymentMethodId, confirm: true, off_session: true } : {}),
305
+ metadata: item.metadata
306
+ },
307
+ idemKey
308
+ )
309
+ const chargeId = String(res.id ?? `pi_${Date.now()}`)
310
+ const status = res.status === 'succeeded' ? 'succeeded' : res.status === 'canceled' ? 'failed' : 'requires_action'
311
+ if (status === 'succeeded' && item.kind === 'product') await this.applyProductGrants(userId, item.id, chargeId)
312
+ return {
313
+ chargeId,
314
+ status,
315
+ amount: totalAmount,
316
+ currency: item.currency,
317
+ provider: 'stripe',
318
+ ...(res.next_action?.redirect_to_url?.url ? { checkoutUrl: String(res.next_action.redirect_to_url.url) } : {}),
319
+ ...(res.client_secret ? { clientSecret: String(res.client_secret) } : {})
320
+ }
321
+ }
322
+ const res = await this.dodoCall(
323
+ 'POST',
324
+ '/payments',
325
+ {
326
+ customer: { customer_id: providerCustomerId },
327
+ payment_link: !parsed.paymentMethodId,
328
+ ...(parsed.paymentMethodId ? { payment_method_id: parsed.paymentMethodId } : {}),
329
+ ...buildDodoCartOrCustom(item),
330
+ metadata: item.metadata
331
+ },
332
+ idemKey
333
+ )
334
+ const chargeId = String(res.payment_id ?? res.id ?? `pay_${Date.now()}`)
335
+ const status = res.status === 'succeeded' ? 'succeeded' : res.status === 'failed' ? 'failed' : 'requires_action'
336
+ if (status === 'succeeded' && item.kind === 'product') await this.applyProductGrants(userId, item.id, chargeId)
337
+ return {
338
+ chargeId,
339
+ status,
340
+ amount: totalAmount,
341
+ currency: item.currency,
342
+ provider: 'dodo',
343
+ ...(res.payment_link ? { checkoutUrl: String(res.payment_link) } : {})
344
+ }
345
+ }
346
+
347
+ subscribe = async (rawInput: SubscribeInput, opts?: PaymentCallOptions): Promise<SubscribeResult> => {
348
+ const parsed = subscribeInput.parse(rawInput)
349
+ const userId = opts?.userId ?? this.#core.userId
350
+ const provider = this.resolveProvider(opts?.provider)
351
+ const isDev = Boolean(this.#core.env.isDev)
352
+ const plan = getPlanDefinition(parsed.plan)
353
+ if (!plan) return this.#core.errors.httpError(400, `Unknown plan alias: ${parsed.plan}`, 'INVALID_PLAN')
354
+
355
+ const { providerCustomerId } = await this.ensureCustomer(userId, provider)
356
+ const returnUrl = parsed.returnUrl ?? resolveEnvScopedValue(this.#core.config.payments.urls.returnUrl, isDev) ?? 'https://localhost/billing'
357
+ const idemKey = idempotencyPaymentKey('sub', parsed.idempotencyKey ?? opts?.idempotencyKey)
358
+
359
+ if (plan.interval === 'free' || plan.amount === 0 || this.getCreds(provider).isSyntheticDev) {
360
+ const subscriptionId = `sub_dev_${plan.id}_${userId}`
361
+ await this.#core.db.update(
362
+ 'payment_customer',
363
+ userId,
364
+ { planId: plan.id, subscriptionId, subscriptionStatus: 'active', activeProvider: provider, cancelAtPeriodEnd: false },
365
+ { bypassRbac: true }
366
+ )
367
+ return { subscriptionId, planId: plan.id, status: 'active', provider }
368
+ }
369
+ const item = this.#resolveItem({ plan: plan.id, metadata: parsed.metadata }, userId)
370
+ const res =
371
+ provider === 'stripe'
372
+ ? await this.stripeCall(
373
+ 'POST',
374
+ '/subscriptions',
375
+ {
376
+ customer: providerCustomerId,
377
+ items: [{ ...(item.stripePriceId ? { price: item.stripePriceId } : buildStripeLineItem(item)) }],
378
+ payment_behavior: 'default_incomplete',
379
+ metadata: item.metadata
380
+ },
381
+ idemKey
382
+ )
383
+ : await this.dodoCall(
384
+ 'POST',
385
+ '/subscriptions',
386
+ { customer: { customer_id: providerCustomerId }, product_id: item.dodoProductId ?? plan.id, quantity: 1, payment_link: true, return_url: returnUrl, metadata: item.metadata },
387
+ idemKey
388
+ )
389
+ const subscriptionId = String(res.subscription_id ?? res.id ?? `sub_${Date.now()}`)
390
+ const status = res.status === 'active' ? 'active' : res.status === 'trialing' ? 'trialing' : 'requires_action'
391
+ const checkoutUrl = res.payment_link ?? res.checkout_url
392
+ await this.#core.db.update(
393
+ 'payment_customer',
394
+ userId,
395
+ { planId: plan.id, subscriptionId, subscriptionStatus: status === 'requires_action' ? 'none' : status, activeProvider: provider },
396
+ { bypassRbac: true }
397
+ )
398
+ return { subscriptionId, planId: plan.id, status, provider, ...(checkoutUrl ? { checkoutUrl: String(checkoutUrl) } : {}) }
399
+ }
400
+
401
+ changeSubscriptionTier = async (rawInput: ChangeTierInput, opts?: PaymentCallOptions): Promise<SubscriptionRecord> => {
402
+ const parsed = changeTierInput.parse(rawInput)
403
+ const userId = opts?.userId ?? this.#core.userId
404
+ const plan = getPlanDefinition(parsed.plan)
405
+ if (!plan) return this.#core.errors.httpError(400, `Unknown plan alias: ${parsed.plan}`, 'INVALID_PLAN')
406
+
407
+ const cust = await this.#core.db.read('payment_customer', userId, false, { bypassRbac: true })
408
+ if (!cust?.subscriptionId) return this.#core.errors.httpError(400, 'No active subscription to change tier', 'NO_ACTIVE_SUBSCRIPTION')
409
+ const provider = this.resolveProvider(opts?.provider ?? cust.activeProvider)
410
+ const isDev = Boolean(this.#core.env.isDev)
411
+ const idemKey = idempotencyPaymentKey('tier', parsed.idempotencyKey ?? opts?.idempotencyKey)
412
+
413
+ if (!this.getCreds(provider).isSyntheticDev) {
414
+ if (provider === 'stripe') {
415
+ const sub = await this.stripeCall('GET', `/subscriptions/${cust.subscriptionId}`)
416
+ const itemId = sub.items?.data?.[0]?.id
417
+ const priceId = resolveEnvScopedValue(plan.providers?.stripe?.priceId, isDev) ?? plan.id
418
+ const prorationBehavior = parsed.proration === 'prorated_immediately' ? 'always_invoice' : parsed.proration === 'next_cycle' ? 'create_prorations' : 'none'
419
+ await this.stripeCall(
420
+ 'POST',
421
+ `/subscriptions/${cust.subscriptionId}`,
422
+ { items: [{ ...(itemId ? { id: itemId } : {}), price: priceId }], proration_behavior: prorationBehavior, metadata: { userId, planId: plan.id } },
423
+ idemKey
424
+ )
425
+ } else {
426
+ const productId = resolveEnvScopedValue(plan.providers?.dodo?.productId, isDev) ?? plan.id
427
+ await this.dodoCall(
428
+ 'POST',
429
+ `/subscriptions/${cust.subscriptionId}/change-plan`,
430
+ { product_id: productId, proration_billing_mode: parsed.proration, quantity: 1 },
431
+ idemKey
432
+ )
433
+ }
434
+ }
435
+
436
+ await this.#core.db.update('payment_customer', userId, { planId: plan.id, subscriptionStatus: 'active', cancelAtPeriodEnd: false }, { bypassRbac: true })
437
+ const usageId = paymentUsageDocId(userId, currentPeriodKey(cust.currentPeriodStart))
438
+ const usageDoc = await this.#core.db.read('payment_usage', usageId, false, { bypassRbac: true })
439
+ if (usageDoc) await this.#core.db.update('payment_usage', usageId, { planId: plan.id }, { bypassRbac: true })
440
+
441
+ return {
442
+ subscriptionId: cust.subscriptionId,
443
+ planId: plan.id,
444
+ status: 'active',
445
+ provider,
446
+ cancelAtPeriodEnd: false,
447
+ ...(cust.currentPeriodStart ? { currentPeriodStart: cust.currentPeriodStart } : {}),
448
+ ...(cust.currentPeriodEnd ? { currentPeriodEnd: cust.currentPeriodEnd } : {})
449
+ }
450
+ }
451
+
452
+ cancelSubscription = async (rawInput: CancelSubscriptionInput = {}, opts?: PaymentCallOptions): Promise<SubscriptionRecord> => {
453
+ const parsed = cancelSubscriptionInput.parse(rawInput)
454
+ const userId = opts?.userId ?? this.#core.userId
455
+ const cust = await this.#core.db.read('payment_customer', userId, false, { bypassRbac: true })
456
+ if (!cust?.subscriptionId) return this.#core.errors.httpError(400, 'No active subscription to cancel', 'NO_ACTIVE_SUBSCRIPTION')
457
+ const provider = this.resolveProvider(opts?.provider ?? cust.activeProvider)
458
+ const idemKey = idempotencyPaymentKey('can', parsed.idempotencyKey ?? opts?.idempotencyKey)
459
+
460
+ if (!this.getCreds(provider).isSyntheticDev) {
461
+ if (provider === 'stripe') {
462
+ await (parsed.immediately
463
+ ? this.stripeCall('DELETE', `/subscriptions/${cust.subscriptionId}`, undefined, idemKey)
464
+ : this.stripeCall('POST', `/subscriptions/${cust.subscriptionId}`, { cancel_at_period_end: true }, idemKey))
465
+ } else {
466
+ await this.dodoCall(
467
+ 'PATCH',
468
+ `/subscriptions/${cust.subscriptionId}`,
469
+ parsed.immediately ? { status: 'cancelled' } : { cancel_at_next_billing_date: true },
470
+ idemKey
471
+ )
472
+ }
473
+ }
474
+
475
+ const newStatus = parsed.immediately ? 'cancelled' : cust.subscriptionStatus === 'none' ? 'active' : cust.subscriptionStatus
476
+ const newPlanId = parsed.immediately ? this.#core.config.payments.defaultPlanId : cust.planId
477
+ await this.#core.db.update('payment_customer', userId, { planId: newPlanId, subscriptionStatus: newStatus, cancelAtPeriodEnd: !parsed.immediately }, { bypassRbac: true })
478
+ return {
479
+ subscriptionId: cust.subscriptionId,
480
+ planId: newPlanId,
481
+ status: newStatus,
482
+ provider,
483
+ cancelAtPeriodEnd: !parsed.immediately,
484
+ ...(cust.currentPeriodStart ? { currentPeriodStart: cust.currentPeriodStart } : {}),
485
+ ...(cust.currentPeriodEnd ? { currentPeriodEnd: cust.currentPeriodEnd } : {})
486
+ }
487
+ }
488
+ }
@@ -0,0 +1,3 @@
1
+ export * from './schema'
2
+ export { PaymentsService } from './service'
3
+ export { corePaymentRoutes } from './routes'
@@ -0,0 +1,92 @@
1
+ import type { Context } from 'hono'
2
+ import type { AppEnv } from '../config'
3
+ import type { Core } from '../index'
4
+ import {
5
+ cancelSubscriptionInput,
6
+ changeTierInput,
7
+ chargeResultSchema,
8
+ checkoutSessionResultSchema,
9
+ createCheckoutInput,
10
+ createInvoiceInput,
11
+ createPaymentLinkInput,
12
+ directChargeInput,
13
+ invoiceResultSchema,
14
+ paymentLinkResultSchema,
15
+ paymentWebhookParamsSchema,
16
+ subscribeInput,
17
+ subscribeResultSchema,
18
+ subscriptionRecordSchema,
19
+ type CancelSubscriptionInput,
20
+ type ChangeTierInput,
21
+ type CreateCheckoutInput,
22
+ type CreateInvoiceInput,
23
+ type CreatePaymentLinkInput,
24
+ type DirectChargeInput,
25
+ type PaymentWebhookParams,
26
+ type SubscribeInput
27
+ } from './schema'
28
+
29
+ export const corePaymentRoutes = {
30
+ 'payments.checkout': {
31
+ input: createCheckoutInput,
32
+ output: checkoutSessionResultSchema,
33
+ handler: async ({ core, input }: { core: Core; input: CreateCheckoutInput }) => core.payments.createCheckout(input)
34
+ },
35
+ 'payments.paymentLink': {
36
+ input: createPaymentLinkInput,
37
+ output: paymentLinkResultSchema,
38
+ handler: async ({ core, input }: { core: Core; input: CreatePaymentLinkInput }) => core.payments.createPaymentLink(input)
39
+ },
40
+ 'payments.invoice': {
41
+ input: createInvoiceInput,
42
+ output: invoiceResultSchema,
43
+ handler: async ({ core, input }: { core: Core; input: CreateInvoiceInput }) => core.payments.createInvoice(input)
44
+ },
45
+ 'payments.charge': {
46
+ input: directChargeInput,
47
+ output: chargeResultSchema,
48
+ handler: async ({ core, input }: { core: Core; input: DirectChargeInput }) => core.payments.charge(input)
49
+ },
50
+ 'payments.subscribe': {
51
+ input: subscribeInput,
52
+ output: subscribeResultSchema,
53
+ handler: async ({ core, input }: { core: Core; input: SubscribeInput }) => core.payments.subscribe(input)
54
+ },
55
+ 'payments.changeTier': {
56
+ input: changeTierInput,
57
+ output: subscriptionRecordSchema,
58
+ handler: async ({ core, input }: { core: Core; input: ChangeTierInput }) => core.payments.changeSubscriptionTier(input)
59
+ },
60
+ 'payments.cancelSubscription': {
61
+ input: cancelSubscriptionInput,
62
+ output: subscriptionRecordSchema,
63
+ handler: async ({ core, input }: { core: Core; input: CancelSubscriptionInput }) => core.payments.cancelSubscription(input)
64
+ },
65
+ 'payments.status': {
66
+ handler: async ({ core }: { core: Core }) => ({
67
+ subscription: await core.payments.getSubscription(),
68
+ usage: await core.payments.getUsage()
69
+ })
70
+ },
71
+ 'payments.webhook': {
72
+ params: paymentWebhookParamsSchema,
73
+ handler: async ({
74
+ core,
75
+ c,
76
+ params,
77
+ rawBody
78
+ }: {
79
+ core: Core
80
+ c: Context<AppEnv>
81
+ params: PaymentWebhookParams
82
+ rawBody?: string
83
+ }) => {
84
+ const headers: Record<string, string | undefined> = {}
85
+ c?.req.raw.headers.forEach((value, key) => {
86
+ headers[key.toLowerCase()] = value
87
+ })
88
+ const body = rawBody ?? (c ? await c.req.text() : '')
89
+ return core.internals.payments.verifyAndProcessWebhook(params.provider, body, headers)
90
+ }
91
+ }
92
+ }