@adonis-agora/payments 0.2.0 → 0.3.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 (319) hide show
  1. package/CHANGELOG.md +2347 -0
  2. package/dist/assets/spa/assets/index-CT5Zon6P.css +1 -0
  3. package/dist/assets/spa/assets/index-CvEbJEnq.js +41 -0
  4. package/dist/assets/spa/assets/index-CvEbJEnq.js.map +1 -0
  5. package/dist/assets/spa/index.html +20 -0
  6. package/dist/commands/main.d.ts +4 -3
  7. package/dist/commands/main.d.ts.map +1 -1
  8. package/dist/commands/main.js +9 -2
  9. package/dist/commands/main.js.map +1 -1
  10. package/dist/commands/payments_health.d.ts +31 -0
  11. package/dist/commands/payments_health.d.ts.map +1 -0
  12. package/dist/commands/payments_health.js +113 -0
  13. package/dist/commands/payments_health.js.map +1 -0
  14. package/dist/commands/payments_sync.d.ts +18 -4
  15. package/dist/commands/payments_sync.d.ts.map +1 -1
  16. package/dist/commands/payments_sync.js +136 -40
  17. package/dist/commands/payments_sync.js.map +1 -1
  18. package/dist/configure.d.ts +12 -5
  19. package/dist/configure.d.ts.map +1 -1
  20. package/dist/configure.js +18 -5
  21. package/dist/configure.js.map +1 -1
  22. package/dist/providers/dashboard_provider.d.ts +97 -0
  23. package/dist/providers/dashboard_provider.d.ts.map +1 -0
  24. package/dist/providers/dashboard_provider.js +430 -0
  25. package/dist/providers/dashboard_provider.js.map +1 -0
  26. package/dist/providers/payments_client_provider.d.ts +41 -0
  27. package/dist/providers/payments_client_provider.d.ts.map +1 -0
  28. package/dist/providers/payments_client_provider.js +95 -0
  29. package/dist/providers/payments_client_provider.js.map +1 -0
  30. package/dist/providers/payments_provider.d.ts +35 -0
  31. package/dist/providers/payments_provider.d.ts.map +1 -1
  32. package/dist/providers/payments_provider.js +250 -36
  33. package/dist/providers/payments_provider.js.map +1 -1
  34. package/dist/src/billing/billing_health.d.ts +85 -0
  35. package/dist/src/billing/billing_health.d.ts.map +1 -0
  36. package/dist/src/billing/billing_health.js +155 -0
  37. package/dist/src/billing/billing_health.js.map +1 -0
  38. package/dist/src/billing/billing_overview.d.ts +4 -3
  39. package/dist/src/billing/billing_overview.d.ts.map +1 -1
  40. package/dist/src/billing/billing_overview.js +14 -5
  41. package/dist/src/billing/billing_overview.js.map +1 -1
  42. package/dist/src/billing/billing_store.d.ts +634 -5
  43. package/dist/src/billing/billing_store.d.ts.map +1 -1
  44. package/dist/src/billing/billing_store.js +18 -1
  45. package/dist/src/billing/billing_store.js.map +1 -1
  46. package/dist/src/billing/index.d.ts +2 -1
  47. package/dist/src/billing/index.d.ts.map +1 -1
  48. package/dist/src/billing/index.js +1 -0
  49. package/dist/src/billing/index.js.map +1 -1
  50. package/dist/src/billing/list_query.d.ts +16 -0
  51. package/dist/src/billing/list_query.d.ts.map +1 -0
  52. package/dist/src/billing/list_query.js +28 -0
  53. package/dist/src/billing/list_query.js.map +1 -0
  54. package/dist/src/billing/lucid_billing_store.d.ts +122 -4
  55. package/dist/src/billing/lucid_billing_store.d.ts.map +1 -1
  56. package/dist/src/billing/lucid_billing_store.js +811 -13
  57. package/dist/src/billing/lucid_billing_store.js.map +1 -1
  58. package/dist/src/billing/mixins/index.d.ts +3 -0
  59. package/dist/src/billing/mixins/index.d.ts.map +1 -1
  60. package/dist/src/billing/mixins/index.js +3 -0
  61. package/dist/src/billing/mixins/index.js.map +1 -1
  62. package/dist/src/billing/mixins/with_audit_event.d.ts +48 -0
  63. package/dist/src/billing/mixins/with_audit_event.d.ts.map +1 -0
  64. package/dist/src/billing/mixins/with_audit_event.js +77 -0
  65. package/dist/src/billing/mixins/with_audit_event.js.map +1 -0
  66. package/dist/src/billing/mixins/with_customer.d.ts +39 -0
  67. package/dist/src/billing/mixins/with_customer.d.ts.map +1 -0
  68. package/dist/src/billing/mixins/with_customer.js +68 -0
  69. package/dist/src/billing/mixins/with_customer.js.map +1 -0
  70. package/dist/src/billing/mixins/with_dispute.d.ts +68 -0
  71. package/dist/src/billing/mixins/with_dispute.d.ts.map +1 -0
  72. package/dist/src/billing/mixins/with_dispute.js +83 -0
  73. package/dist/src/billing/mixins/with_dispute.js.map +1 -0
  74. package/dist/src/billing/mixins/with_payment.d.ts +42 -0
  75. package/dist/src/billing/mixins/with_payment.d.ts.map +1 -1
  76. package/dist/src/billing/mixins/with_payment.js +29 -3
  77. package/dist/src/billing/mixins/with_payment.js.map +1 -1
  78. package/dist/src/billing/mixins/with_subscription.d.ts +7 -0
  79. package/dist/src/billing/mixins/with_subscription.d.ts.map +1 -1
  80. package/dist/src/billing/mixins/with_subscription.js +15 -1
  81. package/dist/src/billing/mixins/with_subscription.js.map +1 -1
  82. package/dist/src/billing/mixins/with_usage_event.d.ts +7 -0
  83. package/dist/src/billing/mixins/with_usage_event.d.ts.map +1 -1
  84. package/dist/src/billing/mixins/with_usage_event.js +15 -1
  85. package/dist/src/billing/mixins/with_usage_event.js.map +1 -1
  86. package/dist/src/billing/mixins/with_webhook_event.d.ts +18 -0
  87. package/dist/src/billing/mixins/with_webhook_event.d.ts.map +1 -1
  88. package/dist/src/billing/mixins/with_webhook_event.js +18 -1
  89. package/dist/src/billing/mixins/with_webhook_event.js.map +1 -1
  90. package/dist/src/billing/resolve_dispatch.d.ts +22 -0
  91. package/dist/src/billing/resolve_dispatch.d.ts.map +1 -0
  92. package/dist/src/billing/resolve_dispatch.js +44 -0
  93. package/dist/src/billing/resolve_dispatch.js.map +1 -0
  94. package/dist/src/billing/resolve_store.d.ts +18 -0
  95. package/dist/src/billing/resolve_store.d.ts.map +1 -0
  96. package/dist/src/billing/resolve_store.js +35 -0
  97. package/dist/src/billing/resolve_store.js.map +1 -0
  98. package/dist/src/billing/schema.d.ts +108 -0
  99. package/dist/src/billing/schema.d.ts.map +1 -0
  100. package/dist/src/billing/schema.js +350 -0
  101. package/dist/src/billing/schema.js.map +1 -0
  102. package/dist/src/billing/webhook_dispatcher.d.ts +95 -16
  103. package/dist/src/billing/webhook_dispatcher.d.ts.map +1 -1
  104. package/dist/src/billing/webhook_dispatcher.js +186 -33
  105. package/dist/src/billing/webhook_dispatcher.js.map +1 -1
  106. package/dist/src/billing/webhook_events.d.ts +67 -1
  107. package/dist/src/billing/webhook_events.d.ts.map +1 -1
  108. package/dist/src/billing/webhook_events.js +15 -0
  109. package/dist/src/billing/webhook_events.js.map +1 -1
  110. package/dist/src/billing/webhook_processor.d.ts.map +1 -1
  111. package/dist/src/billing/webhook_processor.js +325 -5
  112. package/dist/src/billing/webhook_processor.js.map +1 -1
  113. package/dist/src/client/define_config.d.ts +150 -0
  114. package/dist/src/client/define_config.d.ts.map +1 -0
  115. package/dist/src/client/define_config.js +98 -0
  116. package/dist/src/client/define_config.js.map +1 -0
  117. package/dist/src/client/handlers.d.ts +71 -0
  118. package/dist/src/client/handlers.d.ts.map +1 -0
  119. package/dist/src/client/handlers.js +119 -0
  120. package/dist/src/client/handlers.js.map +1 -0
  121. package/dist/src/client/index.d.ts +16 -0
  122. package/dist/src/client/index.d.ts.map +1 -0
  123. package/dist/src/client/index.js +13 -0
  124. package/dist/src/client/index.js.map +1 -0
  125. package/dist/src/client/statuses.d.ts +38 -0
  126. package/dist/src/client/statuses.d.ts.map +1 -0
  127. package/dist/src/client/statuses.js +42 -0
  128. package/dist/src/client/statuses.js.map +1 -0
  129. package/dist/src/dashboard/actions.d.ts +143 -0
  130. package/dist/src/dashboard/actions.d.ts.map +1 -0
  131. package/dist/src/dashboard/actions.js +112 -0
  132. package/dist/src/dashboard/actions.js.map +1 -0
  133. package/dist/src/dashboard/auth.d.ts +136 -0
  134. package/dist/src/dashboard/auth.d.ts.map +1 -0
  135. package/dist/src/dashboard/auth.js +152 -0
  136. package/dist/src/dashboard/auth.js.map +1 -0
  137. package/dist/src/dashboard/define_config.d.ts +80 -0
  138. package/dist/src/dashboard/define_config.d.ts.map +1 -0
  139. package/dist/src/dashboard/define_config.js +83 -0
  140. package/dist/src/dashboard/define_config.js.map +1 -0
  141. package/dist/src/dashboard/handlers.d.ts +281 -0
  142. package/dist/src/dashboard/handlers.d.ts.map +1 -0
  143. package/dist/src/dashboard/handlers.js +960 -0
  144. package/dist/src/dashboard/handlers.js.map +1 -0
  145. package/dist/src/dashboard/index.d.ts +18 -0
  146. package/dist/src/dashboard/index.d.ts.map +1 -0
  147. package/dist/src/dashboard/index.js +14 -0
  148. package/dist/src/dashboard/index.js.map +1 -0
  149. package/dist/src/dashboard/login_page.d.ts +23 -0
  150. package/dist/src/dashboard/login_page.d.ts.map +1 -0
  151. package/dist/src/dashboard/login_page.js +170 -0
  152. package/dist/src/dashboard/login_page.js.map +1 -0
  153. package/dist/src/dashboard/period.d.ts +31 -0
  154. package/dist/src/dashboard/period.d.ts.map +1 -0
  155. package/dist/src/dashboard/period.js +44 -0
  156. package/dist/src/dashboard/period.js.map +1 -0
  157. package/dist/src/dashboard/session_cookie.d.ts +42 -0
  158. package/dist/src/dashboard/session_cookie.d.ts.map +1 -0
  159. package/dist/src/dashboard/session_cookie.js +97 -0
  160. package/dist/src/dashboard/session_cookie.js.map +1 -0
  161. package/dist/src/dashboard/spa.d.ts +24 -0
  162. package/dist/src/dashboard/spa.d.ts.map +1 -0
  163. package/dist/src/dashboard/spa.js +52 -0
  164. package/dist/src/dashboard/spa.js.map +1 -0
  165. package/dist/src/define_config.d.ts +244 -9
  166. package/dist/src/define_config.d.ts.map +1 -1
  167. package/dist/src/define_config.js +97 -1
  168. package/dist/src/define_config.js.map +1 -1
  169. package/dist/src/diagnostics.d.ts +287 -7
  170. package/dist/src/diagnostics.d.ts.map +1 -1
  171. package/dist/src/diagnostics.js +317 -1
  172. package/dist/src/diagnostics.js.map +1 -1
  173. package/dist/src/driver.d.ts +139 -5
  174. package/dist/src/driver.d.ts.map +1 -1
  175. package/dist/src/drivers/abacate.d.ts +12 -2
  176. package/dist/src/drivers/abacate.d.ts.map +1 -1
  177. package/dist/src/drivers/abacate.js +112 -19
  178. package/dist/src/drivers/abacate.js.map +1 -1
  179. package/dist/src/drivers/adyen.d.ts +164 -0
  180. package/dist/src/drivers/adyen.d.ts.map +1 -0
  181. package/dist/src/drivers/adyen.js +678 -0
  182. package/dist/src/drivers/adyen.js.map +1 -0
  183. package/dist/src/drivers/asaas.d.ts +52 -2
  184. package/dist/src/drivers/asaas.d.ts.map +1 -1
  185. package/dist/src/drivers/asaas.js +364 -29
  186. package/dist/src/drivers/asaas.js.map +1 -1
  187. package/dist/src/drivers/dodo.d.ts +135 -0
  188. package/dist/src/drivers/dodo.d.ts.map +1 -0
  189. package/dist/src/drivers/dodo.js +864 -0
  190. package/dist/src/drivers/dodo.js.map +1 -0
  191. package/dist/src/drivers/efi.d.ts +98 -0
  192. package/dist/src/drivers/efi.d.ts.map +1 -0
  193. package/dist/src/drivers/efi.js +628 -0
  194. package/dist/src/drivers/efi.js.map +1 -0
  195. package/dist/src/drivers/infinitepay.d.ts +156 -0
  196. package/dist/src/drivers/infinitepay.d.ts.map +1 -0
  197. package/dist/src/drivers/infinitepay.js +321 -0
  198. package/dist/src/drivers/infinitepay.js.map +1 -0
  199. package/dist/src/drivers/lemonsqueezy.d.ts +125 -0
  200. package/dist/src/drivers/lemonsqueezy.d.ts.map +1 -0
  201. package/dist/src/drivers/lemonsqueezy.js +632 -0
  202. package/dist/src/drivers/lemonsqueezy.js.map +1 -0
  203. package/dist/src/drivers/mercadopago.d.ts +97 -0
  204. package/dist/src/drivers/mercadopago.d.ts.map +1 -0
  205. package/dist/src/drivers/mercadopago.js +844 -0
  206. package/dist/src/drivers/mercadopago.js.map +1 -0
  207. package/dist/src/drivers/mollie.d.ts +125 -0
  208. package/dist/src/drivers/mollie.d.ts.map +1 -0
  209. package/dist/src/drivers/mollie.js +812 -0
  210. package/dist/src/drivers/mollie.js.map +1 -0
  211. package/dist/src/drivers/paddle.d.ts +148 -0
  212. package/dist/src/drivers/paddle.d.ts.map +1 -0
  213. package/dist/src/drivers/paddle.js +738 -0
  214. package/dist/src/drivers/paddle.js.map +1 -0
  215. package/dist/src/drivers/pagarme.d.ts +97 -0
  216. package/dist/src/drivers/pagarme.d.ts.map +1 -0
  217. package/dist/src/drivers/pagarme.js +829 -0
  218. package/dist/src/drivers/pagarme.js.map +1 -0
  219. package/dist/src/drivers/pagbank.d.ts +57 -0
  220. package/dist/src/drivers/pagbank.d.ts.map +1 -0
  221. package/dist/src/drivers/pagbank.js +499 -0
  222. package/dist/src/drivers/pagbank.js.map +1 -0
  223. package/dist/src/drivers/paypal.d.ts +120 -0
  224. package/dist/src/drivers/paypal.d.ts.map +1 -0
  225. package/dist/src/drivers/paypal.js +891 -0
  226. package/dist/src/drivers/paypal.js.map +1 -0
  227. package/dist/src/drivers/polar.d.ts +118 -0
  228. package/dist/src/drivers/polar.d.ts.map +1 -0
  229. package/dist/src/drivers/polar.js +604 -0
  230. package/dist/src/drivers/polar.js.map +1 -0
  231. package/dist/src/drivers/razorpay.d.ts +103 -0
  232. package/dist/src/drivers/razorpay.d.ts.map +1 -0
  233. package/dist/src/drivers/razorpay.js +867 -0
  234. package/dist/src/drivers/razorpay.js.map +1 -0
  235. package/dist/src/drivers/shared.d.ts +33 -0
  236. package/dist/src/drivers/shared.d.ts.map +1 -0
  237. package/dist/src/drivers/shared.js +37 -0
  238. package/dist/src/drivers/shared.js.map +1 -0
  239. package/dist/src/drivers/square.d.ts +133 -0
  240. package/dist/src/drivers/square.d.ts.map +1 -0
  241. package/dist/src/drivers/square.js +905 -0
  242. package/dist/src/drivers/square.js.map +1 -0
  243. package/dist/src/drivers/stripe.d.ts +40 -3
  244. package/dist/src/drivers/stripe.d.ts.map +1 -1
  245. package/dist/src/drivers/stripe.js +806 -25
  246. package/dist/src/drivers/stripe.js.map +1 -1
  247. package/dist/src/drivers/woovi.d.ts +10 -3
  248. package/dist/src/drivers/woovi.d.ts.map +1 -1
  249. package/dist/src/drivers/woovi.js +162 -29
  250. package/dist/src/drivers/woovi.js.map +1 -1
  251. package/dist/src/ensure_customer.d.ts +34 -1
  252. package/dist/src/ensure_customer.d.ts.map +1 -1
  253. package/dist/src/ensure_customer.js +29 -4
  254. package/dist/src/ensure_customer.js.map +1 -1
  255. package/dist/src/http.d.ts +37 -3
  256. package/dist/src/http.d.ts.map +1 -1
  257. package/dist/src/http.js +69 -5
  258. package/dist/src/http.js.map +1 -1
  259. package/dist/src/index.d.ts +40 -10
  260. package/dist/src/index.d.ts.map +1 -1
  261. package/dist/src/index.js +22 -4
  262. package/dist/src/index.js.map +1 -1
  263. package/dist/src/invoice/emit_invoice.d.ts.map +1 -1
  264. package/dist/src/invoice/emit_invoice.js +31 -11
  265. package/dist/src/invoice/emit_invoice.js.map +1 -1
  266. package/dist/src/money.d.ts +23 -5
  267. package/dist/src/money.d.ts.map +1 -1
  268. package/dist/src/money.js +72 -7
  269. package/dist/src/money.js.map +1 -1
  270. package/dist/src/payments_manager.d.ts +8 -1
  271. package/dist/src/payments_manager.d.ts.map +1 -1
  272. package/dist/src/payments_manager.js +168 -6
  273. package/dist/src/payments_manager.js.map +1 -1
  274. package/dist/src/services/main.d.ts +38 -0
  275. package/dist/src/services/main.d.ts.map +1 -1
  276. package/dist/src/services/main.js +130 -0
  277. package/dist/src/services/main.js.map +1 -1
  278. package/dist/src/telescope/index.d.ts +2 -0
  279. package/dist/src/telescope/index.d.ts.map +1 -1
  280. package/dist/src/telescope/index.js +1 -0
  281. package/dist/src/telescope/index.js.map +1 -1
  282. package/dist/src/telescope/payments_watcher.d.ts +10 -4
  283. package/dist/src/telescope/payments_watcher.d.ts.map +1 -1
  284. package/dist/src/telescope/payments_watcher.js +65 -13
  285. package/dist/src/telescope/payments_watcher.js.map +1 -1
  286. package/dist/src/telescope/register.d.ts +62 -0
  287. package/dist/src/telescope/register.d.ts.map +1 -0
  288. package/dist/src/telescope/register.js +48 -0
  289. package/dist/src/telescope/register.js.map +1 -0
  290. package/dist/src/testing/fake_payments.d.ts +62 -0
  291. package/dist/src/testing/fake_payments.d.ts.map +1 -0
  292. package/dist/src/testing/fake_payments.js +82 -0
  293. package/dist/src/testing/fake_payments.js.map +1 -0
  294. package/dist/src/testing/in_memory_billing_store.d.ts +151 -2
  295. package/dist/src/testing/in_memory_billing_store.d.ts.map +1 -1
  296. package/dist/src/testing/in_memory_billing_store.js +517 -4
  297. package/dist/src/testing/in_memory_billing_store.js.map +1 -1
  298. package/dist/src/testing/index.d.ts +2 -1
  299. package/dist/src/testing/index.d.ts.map +1 -1
  300. package/dist/src/testing/index.js +1 -0
  301. package/dist/src/testing/index.js.map +1 -1
  302. package/dist/src/types.d.ts +161 -5
  303. package/dist/src/types.d.ts.map +1 -1
  304. package/dist/src/types.js +30 -1
  305. package/dist/src/types.js.map +1 -1
  306. package/dist/src/webhook_handlers.d.ts +96 -2
  307. package/dist/src/webhook_handlers.d.ts.map +1 -1
  308. package/dist/src/webhook_handlers.js +91 -10
  309. package/dist/src/webhook_handlers.js.map +1 -1
  310. package/dist/src/webhook_security.d.ts +94 -3
  311. package/dist/src/webhook_security.d.ts.map +1 -1
  312. package/dist/src/webhook_security.js +173 -9
  313. package/dist/src/webhook_security.js.map +1 -1
  314. package/dist/stubs/config/payments.stub +13 -0
  315. package/dist/stubs/config/payments_client.stub +42 -0
  316. package/dist/stubs/config/payments_dashboard.stub +38 -0
  317. package/dist/stubs/database/migrations/create_billing_tables.stub +35 -77
  318. package/dist/stubs/make/webhook_handler/main.stub +29 -11
  319. package/package.json +60 -25
@@ -0,0 +1,350 @@
1
+ /**
2
+ * The billing schema, as standalone idempotent functions.
3
+ *
4
+ * This is the ecosystem convention — a lib owns its own schema — and it is why
5
+ * `@adonis-agora/durable` exports `createDurableTables` and `@adonis-agora/authz` exports
6
+ * `createAuthzTables`. This package did not follow it, and the cost showed up quickly: every
7
+ * column added after the first release became another migration file with hand-written
8
+ * `hasColumn` / `hasTable` guards, and an app adopting the library had three files to publish
9
+ * and run before it could take a single payment.
10
+ *
11
+ * Two paths, one DDL. {@link LucidBillingStore} calls {@link createBillingTables} once on
12
+ * first use (`autoCreateSchema`, on by default), and the published migration stub calls the
13
+ * SAME function — so the two can never drift, which is the whole reason the DDL lives here
14
+ * rather than in a stub the store knows nothing about.
15
+ *
16
+ * The DDL is portable `rawQuery` (`CREATE TABLE IF NOT EXISTS`) rather than Knex's schema
17
+ * builder, matching the posture authz takes: it keeps the `@adonisjs/lucid` coupling to the
18
+ * `rawQuery` surface, and it is idempotent by construction — which is what lets an install
19
+ * that already ran the old Knex migration call this and have it do nothing.
20
+ */
21
+ /**
22
+ * Every table the billing layer owns.
23
+ *
24
+ * Deliberately NOT configurable, unlike authz's table names. The models are the source of
25
+ * truth here (`BillingModels` lets an app swap any of them), and DDL that could name a table
26
+ * the models do not read is a way to create an empty table beside a missing one. An app that
27
+ * renames a table swaps the model and writes its own migration.
28
+ */
29
+ export const BILLING_TABLES = [
30
+ 'billing_customers',
31
+ 'billing_subscriptions',
32
+ 'billing_payments',
33
+ 'billing_webhook_events',
34
+ 'billing_disputes',
35
+ 'billing_usage_events',
36
+ 'billing_audit_events',
37
+ ];
38
+ /** Best-effort dialect name; `undefined` when it cannot be read. */
39
+ export function detectDialect(db) {
40
+ try {
41
+ const direct = db.dialect?.name;
42
+ if (typeof direct === 'string' && direct.length > 0)
43
+ return direct;
44
+ return db.connection?.()?.dialect?.name;
45
+ }
46
+ catch {
47
+ return undefined;
48
+ }
49
+ }
50
+ function isPostgres(dialect) {
51
+ return !!dialect && /postgres|pg|redshift/i.test(dialect);
52
+ }
53
+ function isMysql(dialect) {
54
+ return !!dialect && /mysql|mariadb/i.test(dialect);
55
+ }
56
+ function typesFor(dialect) {
57
+ if (isPostgres(dialect))
58
+ return { timestamp: 'TIMESTAMPTZ', json: 'JSONB' };
59
+ if (isMysql(dialect))
60
+ return { timestamp: 'DATETIME', json: 'JSON' };
61
+ // SQLite: no native JSON or timestamp types. It stores both as TEXT and Lucid serializes
62
+ // on the way in, which is why the models declare `prepare`/`consume` rather than trusting
63
+ // the column type.
64
+ return { timestamp: 'DATETIME', json: 'TEXT' };
65
+ }
66
+ /**
67
+ * Create a secondary index, idempotently.
68
+ *
69
+ * Postgres and SQLite take `CREATE INDEX IF NOT EXISTS`. **MySQL does not** — it fails with
70
+ * a syntax error — so there the statement is attempted and a duplicate-name error is
71
+ * swallowed. That is narrower than it looks: the only way this statement fails on a second
72
+ * run is that the index is already there, which is the outcome asked for. Any other failure
73
+ * still throws.
74
+ */
75
+ async function createIndex(db, dialect, name, table, columns) {
76
+ if (!isMysql(dialect)) {
77
+ await db.rawQuery(`CREATE INDEX IF NOT EXISTS ${name} ON ${table} (${columns})`);
78
+ return;
79
+ }
80
+ try {
81
+ await db.rawQuery(`CREATE INDEX ${name} ON ${table} (${columns})`);
82
+ }
83
+ catch (error) {
84
+ const message = error instanceof Error ? error.message : String(error);
85
+ if (!/duplicate key name|already exists/i.test(message))
86
+ throw error;
87
+ }
88
+ }
89
+ /**
90
+ * Add a column that was introduced AFTER its table shipped, idempotently.
91
+ *
92
+ * `CREATE TABLE IF NOT EXISTS` is a no-op on a table that already exists, so it cannot carry
93
+ * a new column to an install that ran an earlier version. Without this, upgrading the package
94
+ * would silently leave the old schema in place and the first query naming the new column
95
+ * would fail — which is the failure mode a library owning its own schema exists to prevent.
96
+ *
97
+ * Postgres has `ADD COLUMN IF NOT EXISTS`. SQLite and MySQL do not, so there the statement is
98
+ * attempted and a duplicate-column error is swallowed: the only way this fails twice is that
99
+ * the column is already there, which is the outcome asked for. Anything else still throws.
100
+ */
101
+ async function addColumn(db, dialect, table, column, type) {
102
+ if (isPostgres(dialect)) {
103
+ await db.rawQuery(`ALTER TABLE ${table} ADD COLUMN IF NOT EXISTS ${column} ${type}`);
104
+ return;
105
+ }
106
+ try {
107
+ await db.rawQuery(`ALTER TABLE ${table} ADD COLUMN ${column} ${type}`);
108
+ }
109
+ catch (error) {
110
+ const message = error instanceof Error ? error.message : String(error);
111
+ if (!/duplicate column|already exists/i.test(message))
112
+ throw error;
113
+ }
114
+ }
115
+ /**
116
+ * Create every billing table, idempotently.
117
+ *
118
+ * Safe to call from a Lucid migration's `up()`, and safe to call repeatedly at boot — which
119
+ * is exactly what {@link LucidBillingStore} does when `autoCreateSchema` is on. An install
120
+ * that already ran the older Knex migrations gets no-ops: every statement is
121
+ * `IF NOT EXISTS`.
122
+ *
123
+ * It also carries columns added AFTER a table shipped — see the block at the end. That is
124
+ * what makes this a real upgrade path rather than a first-install convenience: an app that
125
+ * ran the 0.2.0 migration and then upgrades gets the columns the new code reads, without
126
+ * having to be told to publish and run a second migration.
127
+ */
128
+ export async function createBillingTables(db) {
129
+ const dialect = detectDialect(db);
130
+ const t = typesFor(dialect);
131
+ const run = (sql) => db.rawQuery(sql);
132
+ await run(`CREATE TABLE IF NOT EXISTS billing_customers (
133
+ id VARCHAR(36) PRIMARY KEY,
134
+ owner_type VARCHAR(255),
135
+ owner_id VARCHAR(255),
136
+ gateway_id VARCHAR(255) UNIQUE,
137
+ provider VARCHAR(255),
138
+ email VARCHAR(255),
139
+ name VARCHAR(255),
140
+ tax_id VARCHAR(255),
141
+ metadata ${t.json},
142
+ created_at ${t.timestamp} NOT NULL,
143
+ updated_at ${t.timestamp} NOT NULL
144
+ )`);
145
+ await run(`CREATE TABLE IF NOT EXISTS billing_subscriptions (
146
+ id VARCHAR(36) PRIMARY KEY,
147
+ gateway_id VARCHAR(255) UNIQUE,
148
+ provider VARCHAR(255),
149
+ status VARCHAR(255),
150
+ plan_id VARCHAR(255),
151
+ customer_id VARCHAR(255),
152
+ trial_ends_at ${t.timestamp},
153
+ ends_at ${t.timestamp},
154
+ payload ${t.json},
155
+ created_at ${t.timestamp} NOT NULL,
156
+ updated_at ${t.timestamp} NOT NULL
157
+ )`);
158
+ await run(`CREATE TABLE IF NOT EXISTS billing_payments (
159
+ id VARCHAR(36) PRIMARY KEY,
160
+ gateway_id VARCHAR(255) UNIQUE,
161
+ provider VARCHAR(255),
162
+ status VARCHAR(255),
163
+ amount BIGINT,
164
+ currency VARCHAR(3),
165
+ customer_id VARCHAR(255),
166
+ subscription_id VARCHAR(255),
167
+ external_reference VARCHAR(255),
168
+ refunded_amount BIGINT,
169
+ payload ${t.json},
170
+ paid_at ${t.timestamp},
171
+ created_at ${t.timestamp} NOT NULL,
172
+ updated_at ${t.timestamp} NOT NULL
173
+ )`);
174
+ // The key an app actually looks a payment up by — its own order id, not the gateway's. An
175
+ // unindexed lookup here is a sequential scan on a table that grows with every charge.
176
+ await run(`CREATE TABLE IF NOT EXISTS billing_webhook_events (
177
+ id VARCHAR(36) PRIMARY KEY,
178
+ gateway_event_id VARCHAR(255) UNIQUE,
179
+ provider VARCHAR(255),
180
+ type VARCHAR(255),
181
+ status VARCHAR(255),
182
+ payload ${t.json},
183
+ normalized ${t.json},
184
+ error TEXT,
185
+ created_at ${t.timestamp} NOT NULL,
186
+ updated_at ${t.timestamp} NOT NULL
187
+ )`);
188
+ await run(`CREATE TABLE IF NOT EXISTS billing_disputes (
189
+ id VARCHAR(36) PRIMARY KEY,
190
+ gateway_id VARCHAR(255) UNIQUE,
191
+ payment_gateway_id VARCHAR(255),
192
+ provider VARCHAR(255),
193
+ status VARCHAR(255),
194
+ reason VARCHAR(255),
195
+ amount BIGINT,
196
+ currency VARCHAR(3),
197
+ evidence_due_by ${t.timestamp},
198
+ outcome VARCHAR(255),
199
+ opened_at ${t.timestamp},
200
+ closed_at ${t.timestamp},
201
+ payload ${t.json},
202
+ created_at ${t.timestamp} NOT NULL,
203
+ updated_at ${t.timestamp} NOT NULL
204
+ )`);
205
+ // The deadline read — open disputes ordered by the window closing soonest — scans exactly
206
+ // these two columns, in this order.
207
+ // Added in 0.4.0. A NEW table, not a new column, which is why it needs nothing from the ALTER
208
+ // block below: `CREATE TABLE IF NOT EXISTS` carries a table to an existing install exactly as
209
+ // well as to a fresh one. Only columns added to a table that already shipped have the upgrade
210
+ // problem, and only they belong down there.
211
+ await run(`CREATE TABLE IF NOT EXISTS billing_audit_events (
212
+ id VARCHAR(36) PRIMARY KEY,
213
+ action VARCHAR(255),
214
+ actor VARCHAR(255),
215
+ provider VARCHAR(255),
216
+ subject_type VARCHAR(255),
217
+ subject_id VARCHAR(255),
218
+ amount BIGINT,
219
+ currency VARCHAR(3),
220
+ message TEXT,
221
+ metadata ${t.json},
222
+ created_at ${t.timestamp} NOT NULL,
223
+ updated_at ${t.timestamp} NOT NULL
224
+ )`);
225
+ await run(`CREATE TABLE IF NOT EXISTS billing_usage_events (
226
+ id VARCHAR(36) PRIMARY KEY,
227
+ subscription_id VARCHAR(255),
228
+ customer_id VARCHAR(255),
229
+ meter VARCHAR(255),
230
+ quantity INTEGER DEFAULT 1,
231
+ metadata ${t.json},
232
+ recorded_at ${t.timestamp} NOT NULL,
233
+ updated_at ${t.timestamp} NOT NULL
234
+ )`);
235
+ // ── Columns added after their table shipped ────────────────────────────────────────────
236
+ //
237
+ // Placed here, BEFORE the indexes, and the order is load-bearing: an install on an older
238
+ // version has `billing_payments` without `external_reference`, so `CREATE TABLE IF NOT
239
+ // EXISTS` skips the table and indexing a column that does not exist yet fails the whole
240
+ // call. The integration suite caught exactly that, against a real Postgres, on the first
241
+ // run after this function was written.
242
+ //
243
+ // These columns are already declared in the CREATE statements above, so on a FRESH database
244
+ // every statement here is a no-op. They are repeated as ALTERs for the install that already
245
+ // has the table — which is what makes this an upgrade path rather than a first-install
246
+ // convenience.
247
+ //
248
+ // Anything added to a CREATE statement from now on belongs here too, on the same day. A
249
+ // column that exists only above works on every machine that has not shipped yet.
250
+ // 0.3.0 — the app's own reference on a payment, and the normalized event on the ledger row.
251
+ await addColumn(db, dialect, 'billing_payments', 'external_reference', 'VARCHAR(255)');
252
+ await addColumn(db, dialect, 'billing_webhook_events', 'normalized', t.json);
253
+ // 0.4.0 — how much of a payment has been refunded, in the same integer minor units as
254
+ // `amount`. A PARTIAL refund had nowhere to be recorded before this, so the only two
255
+ // options were to overwrite the status with `refunded` (writing off the whole charge) or to
256
+ // drop the event — and the library dropped it, leaving revenue overstated forever.
257
+ // `BIGINT`, matching `amount`, so net revenue is one subtraction and never a division.
258
+ await addColumn(db, dialect, 'billing_payments', 'refunded_amount', 'BIGINT');
259
+ // ── Indexes ───────────────────────────────────────────────────────────────────────────
260
+ //
261
+ // Last, and that is load-bearing twice over. Every table above has to exist, and so does
262
+ // every column added by the block above — `billing_payments_external_reference_idx` names
263
+ // a column an older install does not have until that ALTER runs. Getting this wrong fails
264
+ // the whole call and leaves the schema half-built on every boot; the integration suite
265
+ // caught both orderings against a real Postgres.
266
+ await createIndex(db, dialect, 'billing_customers_owner_idx', 'billing_customers', 'owner_type, owner_id');
267
+ await createIndex(db, dialect, 'billing_subscriptions_customer_idx', 'billing_subscriptions', 'customer_id');
268
+ await createIndex(db, dialect, 'billing_payments_customer_idx', 'billing_payments', 'customer_id');
269
+ await createIndex(db, dialect, 'billing_payments_subscription_idx', 'billing_payments', 'subscription_id');
270
+ await createIndex(db, dialect, 'billing_payments_external_reference_idx', 'billing_payments', 'external_reference');
271
+ await createIndex(db, dialect, 'billing_disputes_payment_idx', 'billing_disputes', 'payment_gateway_id');
272
+ await createIndex(db, dialect, 'billing_disputes_deadline_idx', 'billing_disputes', 'status, evidence_due_by');
273
+ // The two ways the audit trail is read: "what did the endpoint reject in the last day"
274
+ // (the health check) and "what has anyone done to THIS payment" (the timeline).
275
+ await createIndex(db, dialect, 'billing_audit_events_action_idx', 'billing_audit_events', 'action, created_at');
276
+ await createIndex(db, dialect, 'billing_audit_events_subject_idx', 'billing_audit_events', 'subject_type, subject_id');
277
+ await createIndex(db, dialect, 'billing_usage_subscription_meter_idx', 'billing_usage_events', 'subscription_id, meter');
278
+ await createIndex(db, dialect, 'billing_usage_customer_meter_idx', 'billing_usage_events', 'customer_id, meter');
279
+ }
280
+ /**
281
+ * Callbacks that forget a store's memoized "the schema exists" answer.
282
+ *
283
+ * A `LucidBillingStore` creates the tables once and caches the promise, so after
284
+ * {@link dropBillingTables} the store still believes the tables are there and every
285
+ * subsequent query fails with `relation ... does not exist`. The store registers here so the
286
+ * drop can tell it otherwise — a Set of callbacks rather than an import, because the store
287
+ * already imports this module and the reverse would be a cycle.
288
+ */
289
+ const schemaCacheResets = new Set();
290
+ /**
291
+ * Register a "forget the cached schema" callback. Returns the unregister function.
292
+ * Internal — {@link LucidBillingStore} is the only caller.
293
+ */
294
+ export function registerBillingSchemaCache(reset) {
295
+ schemaCacheResets.add(reset);
296
+ return () => {
297
+ schemaCacheResets.delete(reset);
298
+ };
299
+ }
300
+ /**
301
+ * Drop every billing table, newest-dependency-first.
302
+ *
303
+ * Only for a migration's `down()` and for tests. There is no auto-drop and there will not be
304
+ * one: a library that can delete a payments table on a config typo is a library that will.
305
+ *
306
+ * Every live store is told to forget its cached schema afterwards, so a store built before
307
+ * the drop re-creates the tables on its next query instead of querying tables that are gone.
308
+ */
309
+ export async function dropBillingTables(db) {
310
+ for (const table of [...BILLING_TABLES].reverse()) {
311
+ await db.rawQuery(`DROP TABLE IF EXISTS ${table}`);
312
+ }
313
+ for (const reset of schemaCacheResets)
314
+ reset();
315
+ }
316
+ /**
317
+ * Empty every billing table, keeping the schema.
318
+ *
319
+ * What a test suite actually needs between groups, and the reason it is here rather than in
320
+ * every app: dropping the tables invalidates the store (see above), while leaving the ROWS
321
+ * in place means one test's webhook ledger deduplicates the next test's event — the
322
+ * library's own idempotency working perfectly, against the suite. The app that found this
323
+ * hand-wrote it with a `to_regclass` guard; this is that, dialect-agnostically.
324
+ *
325
+ * Order is the reverse of creation, so a foreign key never blocks a delete. `DELETE FROM`
326
+ * rather than `TRUNCATE`: it is the one statement every dialect here spells the same way,
327
+ * and these tables are test-sized.
328
+ */
329
+ export async function truncateBillingTables(db) {
330
+ for (const table of [...BILLING_TABLES].reverse()) {
331
+ // A table that was never created is not an error to a suite that only wants it empty —
332
+ // and asking `information_schema` per table per group costs more than the delete does.
333
+ try {
334
+ await db.rawQuery(`DELETE FROM ${table}`);
335
+ }
336
+ catch (error) {
337
+ if (!isMissingTableError(error))
338
+ throw error;
339
+ }
340
+ }
341
+ }
342
+ /** Whether a driver error is "that table does not exist" rather than a real failure. */
343
+ function isMissingTableError(error) {
344
+ const message = error instanceof Error ? error.message : String(error);
345
+ return (/does not exist|no such table|doesn't exist|Unknown table|Invalid object name/i.test(message) ||
346
+ // Postgres `undefined_table`, MySQL `ER_NO_SUCH_TABLE`.
347
+ error?.code === '42P01' ||
348
+ error?.errno === 1146);
349
+ }
350
+ //# sourceMappingURL=schema.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"schema.js","sourceRoot":"","sources":["../../../src/billing/schema.ts"],"names":[],"mappings":"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"}
@@ -5,20 +5,19 @@ import type { WebhookProcessor } from './webhook_processor.js';
5
5
  *
6
6
  * - `durable` — the event is dispatched as a fire-and-forget durable workflow run
7
7
  * (`@adonis-agora/durable`), so processing survives crashes and retries deterministically.
8
- * - `queue` — the event is dispatched as an `@adonisjs/queue` job (`queueDispatch`).
9
8
  * - `in-process` — the event runs inline (awaited); on failure it is retried with
10
9
  * exponential backoff in the background.
11
10
  * - `auto` — uses `durable` when its provider is registered, else `queue` when the queue
12
11
  * provider is, else `in-process`.
13
12
  */
14
- export type WebhookDispatchMode = 'durable' | 'queue' | 'in-process' | 'auto';
13
+ export type WebhookDispatchMode = 'durable' | 'in-process' | 'auto';
15
14
  export interface WebhookDispatcherOptions {
16
15
  processor: WebhookProcessor;
17
16
  /**
18
17
  * How webhooks are processed. `'auto'` (default) lazily resolves
19
18
  * `@adonis-agora/durable` and uses it when installed AND available (the app has the
20
19
  * durable provider registered), else `@adonisjs/queue` when available, else
21
- * `in-process`. `'durable'`/`'queue'` require their backend (throw when missing).
20
+ * `in-process`. `'durable'` requires its backend (throws when missing).
22
21
  * `'in-process'` never touches either.
23
22
  */
24
23
  mode?: WebhookDispatchMode;
@@ -41,21 +40,54 @@ export interface WebhookDispatcherOptions {
41
40
  */
42
41
  durableAvailable?: () => Promise<boolean>;
43
42
  /**
44
- * Dispatch a webhook event as an `@adonisjs/queue` job. The provider wires this (it owns
45
- * the job class, its container binding and the queue-manager registration). Absent =
46
- * queue mode is unavailable.
43
+ * Resolve the app's durable {@link DurableEngineLike}. The provider wires this from the
44
+ * container; without it there is no engine to register the workflow ON, and durable mode
45
+ * cannot work — see {@link DurableEngineLike} for why registration is not optional.
47
46
  */
48
- queueDispatch?: (event: WebhookEvent) => Promise<{
49
- jobId?: string;
50
- }>;
47
+ durableEngine?: () => Promise<DurableEngineLike>;
51
48
  }
52
- /** The minimal durable surface we use: a workflow class with a static `dispatch`. */
53
- export interface DurableWorkflowLike {
54
- dispatch(input: {
55
- event: WebhookEvent;
56
- }): Promise<{
57
- runId: string;
58
- }>;
49
+ /**
50
+ * The slice of `@adonis-agora/durable`'s `WorkflowEngine` this needs.
51
+ *
52
+ * Both halves are load-bearing. `start` alone is not enough: the engine looks the workflow
53
+ * up by NAME in its registry and throws `workflow … is not registered` for anything it has
54
+ * not been told about, so the webhook workflow has to be REGISTERED before the first
55
+ * dispatch. This class used to build an anonymous `class extends BaseWorkflow` and call its
56
+ * static `dispatch`, which registers nothing and has no `static workflow = { name }` — so
57
+ * every delivery threw `workflow class PaymentsWebhookWorkflow has no registered name`, was
58
+ * collected as a failed event, and the route answered 500. In an app with durable installed
59
+ * — which is exactly what `'auto'` selects — that was every webhook, forever.
60
+ */
61
+ export interface DurableEngineLike {
62
+ register(name: string, version: string, fn: (ctx: unknown, input: never) => Promise<void>): void;
63
+ start(name: string, input: unknown, runId: string): Promise<unknown>;
64
+ }
65
+ /**
66
+ * The durable workflow's identity. Stable and explicit: the engine registry is keyed by
67
+ * name, an in-flight run records the name+version it started on, and a name that changed
68
+ * between deploys would strand runs mid-flight.
69
+ */
70
+ export declare const WEBHOOK_WORKFLOW_NAME = "payments-webhook";
71
+ export declare const WEBHOOK_WORKFLOW_VERSION = "1";
72
+ /** One event of a delivery that did not get through, and why. */
73
+ export interface WebhookDeliveryFailure {
74
+ event: WebhookEvent;
75
+ error: Error;
76
+ }
77
+ /**
78
+ * What became of one webhook DELIVERY — which may carry several events (Adyen's
79
+ * `notificationItems`, Efí's `pix` array).
80
+ *
81
+ * The route turns this into the HTTP status: any failure at all means a non-2xx, because
82
+ * a 2xx is a promise to the gateway that it never has to send this again.
83
+ */
84
+ export interface WebhookDeliveryResult {
85
+ /** How many events the delivery carried. */
86
+ total: number;
87
+ /** How many were processed (in-process) or accepted by durable/queue. */
88
+ dispatched: number;
89
+ /** The ones that threw, in delivery order. Empty on a clean delivery. */
90
+ failures: WebhookDeliveryFailure[];
59
91
  }
60
92
  /**
61
93
  * Runs webhook events through the {@link WebhookProcessor}. In `auto` mode (the default)
@@ -73,7 +105,54 @@ export declare class WebhookDispatcher {
73
105
  dispatch(event: WebhookEvent): Promise<{
74
106
  runId?: string;
75
107
  }>;
108
+ /**
109
+ * Dispatch a whole DELIVERY: one event, or the several a batched envelope carried.
110
+ *
111
+ * **The loop lives here, not in the route.** Everything it has to get right — the ledger
112
+ * claim, the retry policy, which backend runs the work — is already this class's job; the
113
+ * route's job is to turn the outcome into a status code. It also makes the batch testable
114
+ * without standing up an HTTP context.
115
+ *
116
+ * **Sequential, deliberately.** Two events in one envelope routinely touch the same row
117
+ * (an Adyen AUTHORISATION and its CAPTURE; a Pix and its devolução), and running them
118
+ * concurrently would race the ledger claim and the payment upsert against each other,
119
+ * which is how you get a `paid` row overwritten by the `pending` that preceded it. The
120
+ * gateway's order is the order the money moved in, so it is the order they run in.
121
+ *
122
+ * **One failure does not cancel its siblings.** Event 2 of 4 throwing says nothing about
123
+ * events 3 and 4 — they are different payments, and refusing to process them because a
124
+ * neighbour failed loses money for a reason unrelated to them. So every event is attempted
125
+ * and the failures are COLLECTED. The caller is then responsible for the other half:
126
+ * reporting a non-2xx so the gateway redelivers, since a 200 tells it the failed one is
127
+ * done and it is never sent again. The redelivery re-runs only the failed event — the
128
+ * ledger claims a `failed` row again, while the ones that succeeded answer `null` and skip.
129
+ */
130
+ dispatchAll(events: WebhookEvent | WebhookEvent[]): Promise<WebhookDeliveryResult>;
76
131
  /** Whether durable-backed processing is active. */
77
132
  get mode(): WebhookDispatchMode;
133
+ /**
134
+ * Resolve when nothing this dispatcher accepted is still being processed.
135
+ *
136
+ * **The asymmetry this fixes.** In `in-process` mode `dispatchAll` resolves when the event
137
+ * has been PROCESSED, so a test can assert straight after it. In `durable` mode it
138
+ * resolves when the event has been ACCEPTED — the run executes out of band — so the same
139
+ * assertion races the workflow. Every app that pairs `billing.dispatcher` with durable
140
+ * writes the same polling `waitFor(() => ...)` helper to paper over it, and then cannot
141
+ * write a NEGATIVE assertion at all: "nothing was granted" against a poll is a timed
142
+ * sleep, which is slow when it passes and lies when the machine is loaded.
143
+ *
144
+ * `await dispatcher.flushWebhooks()` is that assertion made possible: after it, an
145
+ * unchanged row means the webhook genuinely changed nothing.
146
+ *
147
+ * It waits for the background in-process RETRIES too, which have exactly the same problem
148
+ * — the first attempt is awaited and the retries are not.
149
+ *
150
+ * Only reaches work this process accepted. A worker-role deployment runs the events in
151
+ * another process, and no in-process await can see those; the timeout says so rather than
152
+ * hanging.
153
+ */
154
+ flushWebhooks(options?: {
155
+ timeoutMs?: number;
156
+ }): Promise<void>;
78
157
  }
79
158
  //# sourceMappingURL=webhook_dispatcher.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"webhook_dispatcher.d.ts","sourceRoot":"","sources":["../../../src/billing/webhook_dispatcher.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAChD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAE/D;;;;;;;;;;GAUG;AACH,MAAM,MAAM,mBAAmB,GAAG,SAAS,GAAG,OAAO,GAAG,YAAY,GAAG,MAAM,CAAC;AAE9E,MAAM,WAAW,wBAAwB;IACvC,SAAS,EAAE,gBAAgB,CAAC;IAC5B;;;;;;OAMG;IACH,IAAI,CAAC,EAAE,mBAAmB,CAAC;IAC3B;;;OAGG;IACH,OAAO,CAAC,EAAE;QACR,mDAAmD;QACnD,GAAG,CAAC,EAAE,MAAM,CAAC;QACb,qCAAqC;QACrC,WAAW,CAAC,EAAE,MAAM,CAAC;QACrB,uCAAuC;QACvC,UAAU,CAAC,EAAE,MAAM,CAAC;KACrB,CAAC;IACF;;;;OAIG;IACH,gBAAgB,CAAC,EAAE,MAAM,OAAO,CAAC,OAAO,CAAC,CAAC;IAC1C;;;;OAIG;IACH,aAAa,CAAC,EAAE,CAAC,KAAK,EAAE,YAAY,KAAK,OAAO,CAAC;QAAE,KAAK,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;CACtE;AAED,qFAAqF;AACrF,MAAM,WAAW,mBAAmB;IAClC,QAAQ,CAAC,KAAK,EAAE;QAAE,KAAK,EAAE,YAAY,CAAA;KAAE,GAAG,OAAO,CAAC;QAAE,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;CACtE;AAED;;;;;GAKG;AACH,qBAAa,iBAAiB;;gBAUhB,OAAO,EAAE,wBAAwB;IAS7C;;;OAGG;IACG,QAAQ,CAAC,KAAK,EAAE,YAAY,GAAG,OAAO,CAAC;QAAE,KAAK,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IAUhE,mDAAmD;IACnD,IAAI,IAAI,IAAI,mBAAmB,CAE9B;CAkFF"}
1
+ {"version":3,"file":"webhook_dispatcher.d.ts","sourceRoot":"","sources":["../../../src/billing/webhook_dispatcher.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAChD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAE/D;;;;;;;;;GASG;AACH,MAAM,MAAM,mBAAmB,GAAG,SAAS,GAAG,YAAY,GAAG,MAAM,CAAC;AAEpE,MAAM,WAAW,wBAAwB;IACvC,SAAS,EAAE,gBAAgB,CAAC;IAC5B;;;;;;OAMG;IACH,IAAI,CAAC,EAAE,mBAAmB,CAAC;IAC3B;;;OAGG;IACH,OAAO,CAAC,EAAE;QACR,mDAAmD;QACnD,GAAG,CAAC,EAAE,MAAM,CAAC;QACb,qCAAqC;QACrC,WAAW,CAAC,EAAE,MAAM,CAAC;QACrB,uCAAuC;QACvC,UAAU,CAAC,EAAE,MAAM,CAAC;KACrB,CAAC;IACF;;;;OAIG;IACH,gBAAgB,CAAC,EAAE,MAAM,OAAO,CAAC,OAAO,CAAC,CAAC;IAC1C;;;;OAIG;IACH,aAAa,CAAC,EAAE,MAAM,OAAO,CAAC,iBAAiB,CAAC,CAAC;CAClD;AAED;;;;;;;;;;;GAWG;AACH,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,EAAE,EAAE,CAAC,GAAG,EAAE,OAAO,EAAE,KAAK,EAAE,KAAK,KAAK,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;IACjG,KAAK,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;CACtE;AAED;;;;GAIG;AACH,eAAO,MAAM,qBAAqB,qBAAqB,CAAC;AACxD,eAAO,MAAM,wBAAwB,MAAM,CAAC;AAE5C,iEAAiE;AACjE,MAAM,WAAW,sBAAsB;IACrC,KAAK,EAAE,YAAY,CAAC;IACpB,KAAK,EAAE,KAAK,CAAC;CACd;AAED;;;;;;GAMG;AACH,MAAM,WAAW,qBAAqB;IACpC,4CAA4C;IAC5C,KAAK,EAAE,MAAM,CAAC;IACd,yEAAyE;IACzE,UAAU,EAAE,MAAM,CAAC;IACnB,yEAAyE;IACzE,QAAQ,EAAE,sBAAsB,EAAE,CAAC;CACpC;AAED;;;;;GAKG;AACH,qBAAa,iBAAiB;;gBAqBhB,OAAO,EAAE,wBAAwB;IAU7C;;;OAGG;IACG,QAAQ,CAAC,KAAK,EAAE,YAAY,GAAG,OAAO,CAAC;QAAE,KAAK,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IAKhE;;;;;;;;;;;;;;;;;;;;;OAqBG;IACG,WAAW,CAAC,MAAM,EAAE,YAAY,GAAG,YAAY,EAAE,GAAG,OAAO,CAAC,qBAAqB,CAAC;IA+CxF,mDAAmD;IACnD,IAAI,IAAI,IAAI,mBAAmB,CAE9B;IA8GD;;;;;;;;;;;;;;;;;;;;OAoBG;IACG,aAAa,CAAC,OAAO,GAAE;QAAE,SAAS,CAAC,EAAE,MAAM,CAAA;KAAO,GAAG,OAAO,CAAC,IAAI,CAAC;CAmBzE"}