@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
@@ -1,6 +1,90 @@
1
1
  import Stripe from 'stripe';
2
2
  import { publishPaymentDiagnostics, publishRefundDiagnostics, publishSubscriptionDiagnostics, } from '../diagnostics.js';
3
3
  import { emitInvoiceIfRequested } from '../invoice/emit_invoice.js';
4
+ import { requireCurrency } from './shared.js';
5
+ /**
6
+ * Stripe's dispute evidence fields, verbatim from the Dispute object (API
7
+ * `2025-08-27.basil`).
8
+ *
9
+ * The set is what lets `metadata` reach the twenty-odd fields {@link DisputeEvidence} has
10
+ * no name for — `product_description`, `cancellation_rebuttal`, `access_activity_log` —
11
+ * while a misspelled or invented key is refused here instead of failing the whole
12
+ * submission at the API. A dispute gets ONE submission; it should not be spent on a typo.
13
+ */
14
+ /**
15
+ * {@link DisputeDocumentKind} onto Stripe's nine evidence file fields.
16
+ *
17
+ * Stripe files evidence by what it proves, one File upload id per field, which is why the
18
+ * shared type addresses a document by kind rather than handing over a bare list of ids: an
19
+ * id with no kind cannot be submitted anywhere.
20
+ *
21
+ * Three kinds have no field of their own and land on `uncategorized_file`: Stripe has no
22
+ * invoice or terms-of-service slot, and `other` is what the slot is for. Two of them in one
23
+ * submission collide there, and the collision is reported rather than silently resolved —
24
+ * pick which one Stripe should see, or pass the other under its own field via `metadata`.
25
+ */
26
+ const STRIPE_DOCUMENT_FIELDS = {
27
+ receipt: 'receipt',
28
+ customer_communication: 'customer_communication',
29
+ customer_signature: 'customer_signature',
30
+ shipping: 'shipping_documentation',
31
+ service: 'service_documentation',
32
+ refund_policy: 'refund_policy',
33
+ cancellation_policy: 'cancellation_policy',
34
+ duplicate_charge: 'duplicate_charge_documentation',
35
+ invoice: 'uncategorized_file',
36
+ terms: 'uncategorized_file',
37
+ other: 'uncategorized_file',
38
+ };
39
+ const STRIPE_EVIDENCE_FIELDS = new Set([
40
+ 'access_activity_log',
41
+ 'billing_address',
42
+ 'cancellation_policy',
43
+ 'cancellation_policy_disclosure',
44
+ 'cancellation_rebuttal',
45
+ 'customer_communication',
46
+ 'customer_email_address',
47
+ 'customer_name',
48
+ 'customer_purchase_ip',
49
+ 'customer_signature',
50
+ 'duplicate_charge_documentation',
51
+ 'duplicate_charge_explanation',
52
+ 'duplicate_charge_id',
53
+ 'product_description',
54
+ 'receipt',
55
+ 'refund_policy',
56
+ 'refund_policy_disclosure',
57
+ 'refund_refusal_explanation',
58
+ 'service_date',
59
+ 'service_documentation',
60
+ 'shipping_address',
61
+ 'shipping_carrier',
62
+ 'shipping_date',
63
+ 'shipping_documentation',
64
+ 'shipping_tracking_number',
65
+ 'uncategorized_file',
66
+ 'uncategorized_text',
67
+ ]);
68
+ /**
69
+ * The nine evidence fields that take a **File upload id** (`file_…`, created with purpose
70
+ * `dispute_evidence`) rather than text.
71
+ *
72
+ * This is the mismatch behind most of the refusals below: a URL is neither a file id nor
73
+ * something the reviewing bank will follow — Stripe's own guidance is that banks review no
74
+ * external content, links included — so a driver that put `receiptUrl` in `receipt` would
75
+ * submit a defense the issuer never reads and report it as sent.
76
+ */
77
+ const STRIPE_EVIDENCE_FILE_FIELDS = new Set([
78
+ 'cancellation_policy',
79
+ 'customer_communication',
80
+ 'customer_signature',
81
+ 'duplicate_charge_documentation',
82
+ 'receipt',
83
+ 'refund_policy',
84
+ 'service_documentation',
85
+ 'shipping_documentation',
86
+ 'uncategorized_file',
87
+ ]);
4
88
  /**
5
89
  * Stripe driver. Wraps the Stripe SDK and normalizes its API onto the shared
6
90
  * {@link PaymentsDriver} contract. The SDK is imported lazily by the factory in
@@ -9,7 +93,7 @@ import { emitInvoiceIfRequested } from '../invoice/emit_invoice.js';
9
93
  export class StripeDriver {
10
94
  provider = 'stripe';
11
95
  supportedMethods = ['pix', 'credit_card', 'boleto', 'undefined'];
12
- capabilities = { refunds: true, invoices: true, subscriptions: true };
96
+ capabilities = { disputes: true, refunds: true, invoices: true, subscriptions: true };
13
97
  #stripe;
14
98
  #currency;
15
99
  #webhookSecret;
@@ -23,7 +107,9 @@ export class StripeDriver {
23
107
  throw new Error('[payments] Stripe driver requires an API key. Set `STRIPE_KEY` env or pass `apiKey` to `payments.stripe()`.');
24
108
  }
25
109
  this.#stripe = new Stripe(apiKey);
26
- this.#currency = config.currency ?? 'brl';
110
+ // The constructor (not the factory) enforces this, because `./drivers/stripe` is a
111
+ // public entry point too — this is the one boundary every path crosses.
112
+ this.#currency = requireCurrency('stripe', config.currency);
27
113
  this.#webhookSecret = config.webhookSecret ?? process.env.STRIPE_WEBHOOK_SECRET;
28
114
  }
29
115
  // ── Customers ────────────────────────────────────────────────────────────────────────
@@ -35,7 +121,7 @@ export class StripeDriver {
35
121
  ...(input.metadata !== undefined
36
122
  ? { metadata: input.metadata }
37
123
  : {}),
38
- });
124
+ }, this.#requestOptions(input.idempotencyKey));
39
125
  return this.#mapCustomer(customer);
40
126
  }
41
127
  async findCustomer(customerId) {
@@ -64,6 +150,9 @@ export class StripeDriver {
64
150
  }
65
151
  // ── Payments ─────────────────────────────────────────────────────────────────────────
66
152
  async charge(input) {
153
+ // Without `payment_method_types` Stripe creates the intent with whatever the account's
154
+ // dashboard defaults are, so a charge routed as Pix could come back a card.
155
+ const methodType = this.#mapMethodToStripe(input.method);
67
156
  const params = {
68
157
  amount: input.amount,
69
158
  currency: input.currency ?? this.#currency,
@@ -71,18 +160,20 @@ export class StripeDriver {
71
160
  ...(input.description !== undefined ? { description: input.description } : {}),
72
161
  ...(input.paymentMethodId !== undefined ? { payment_method: input.paymentMethodId } : {}),
73
162
  ...(input.card !== undefined ? { payment_method: input.card.token } : {}),
163
+ ...(methodType !== undefined ? { payment_method_types: [methodType] } : {}),
74
164
  ...(input.metadata !== undefined
75
165
  ? { metadata: input.metadata }
76
166
  : {}),
77
- ...(input.idempotencyKey !== undefined ? {} : {}),
78
167
  };
79
168
  if (input.idempotencyKey !== undefined) {
169
+ // Stripe never echoes the request header back on the object, so the metadata copy is
170
+ // what lets `payment.payload` trace a charge to the key that created it.
80
171
  params.metadata = { ...(params.metadata ?? {}), idempotency_key: input.idempotencyKey };
81
172
  }
82
173
  if (input.externalReference !== undefined) {
83
174
  params.metadata = { ...(params.metadata ?? {}), external_reference: input.externalReference };
84
175
  }
85
- const intent = await this.#stripe.paymentIntents.create(params);
176
+ const intent = await this.#stripe.paymentIntents.create(params, this.#requestOptions(input.idempotencyKey));
86
177
  const payment = this.#mapPayment(intent);
87
178
  await emitInvoiceIfRequested(this.#invoiceCtx, input, payment, this);
88
179
  publishPaymentDiagnostics(payment);
@@ -99,11 +190,11 @@ export class StripeDriver {
99
190
  throw error;
100
191
  }
101
192
  }
102
- async refund(paymentGatewayId, amount) {
193
+ async refund(paymentGatewayId, amount, options) {
103
194
  const refund = await this.#stripe.refunds.create({
104
195
  payment_intent: paymentGatewayId,
105
196
  ...(amount !== undefined ? { amount } : {}),
106
- });
197
+ }, this.#requestOptions(options?.idempotencyKey));
107
198
  const result = {
108
199
  id: refund.id,
109
200
  gatewayId: refund.id,
@@ -149,7 +240,7 @@ export class StripeDriver {
149
240
  ],
150
241
  }),
151
242
  };
152
- const session = await this.#stripe.checkout.sessions.create(params);
243
+ const session = await this.#stripe.checkout.sessions.create(params, this.#requestOptions(input.idempotencyKey));
153
244
  return {
154
245
  id: session.id,
155
246
  gatewayId: session.id,
@@ -195,7 +286,7 @@ export class StripeDriver {
195
286
  }
196
287
  : {}),
197
288
  };
198
- const subscription = await this.#stripe.subscriptions.create(params);
289
+ const subscription = await this.#stripe.subscriptions.create(params, this.#requestOptions(input.idempotencyKey));
199
290
  const result = this.#mapSubscription(subscription);
200
291
  publishSubscriptionDiagnostics(result, 'subscription.created');
201
292
  return result;
@@ -236,7 +327,7 @@ export class StripeDriver {
236
327
  ? { metadata: input.metadata }
237
328
  : {}),
238
329
  };
239
- const subscription = await this.#stripe.subscriptions.update(subscriptionGatewayId, params);
330
+ const subscription = await this.#stripe.subscriptions.update(subscriptionGatewayId, params, this.#requestOptions(input.idempotencyKey));
240
331
  return this.#mapSubscription(subscription);
241
332
  }
242
333
  // ── Invoices ─────────────────────────────────────────────────────────────────────────
@@ -264,7 +355,71 @@ export class StripeDriver {
264
355
  };
265
356
  });
266
357
  }
358
+ // ── Disputes ─────────────────────────────────────────────────────────────────────────
359
+ /**
360
+ * Read a dispute by its Stripe id (`du_…`) — `GET /v1/disputes/{id}`.
361
+ *
362
+ * The two fields an operator acts on come off Stripe's own `evidence_details` rather
363
+ * than off a reading of the status: `evidenceDueBy` from `due_by`, `canSubmitEvidence`
364
+ * from the status **and** `past_due`. `payload` carries the whole Dispute, including
365
+ * `is_charge_refundable` — the other half of the decision, because while it is true a
366
+ * refund can still end the matter, and after a full refund Stripe withdraws nothing
367
+ * further.
368
+ */
369
+ async findDispute(disputeGatewayId) {
370
+ try {
371
+ return this.#mapDispute(await this.#stripe.disputes.retrieve(disputeGatewayId));
372
+ }
373
+ catch (error) {
374
+ if (this.#isNotFound(error))
375
+ return null;
376
+ throw error;
377
+ }
378
+ }
379
+ /**
380
+ * Submit evidence for a dispute — `POST /v1/disputes/{id}` with the `evidence` hash.
381
+ *
382
+ * **Submitting is final.** Stripe forwards the response to the issuing bank immediately;
383
+ * you cannot edit it, add to it, or send a second one. So this refuses in three places
384
+ * rather than guessing: it maps every {@link DisputeEvidence} field or names the one it
385
+ * cannot (silently dropping a field the caller believed was submitted is the worst bug
386
+ * this file could have), it re-reads the dispute and refuses when Stripe will not accept
387
+ * evidence, and it refuses an empty hash rather than spending the one submission on
388
+ * nothing.
389
+ *
390
+ * It never decides *whether* to fight — that is margin, customer value and fraud
391
+ * history, none of which a library can see. `submit` defaults to `true` because the
392
+ * method is named submit; `metadata.submit: false` stages a draft instead, which stays
393
+ * visible in the API and the Dashboard until someone submits it.
394
+ */
395
+ async submitDisputeEvidence(disputeGatewayId, evidence) {
396
+ // Mapped first: a field this driver cannot carry should cost the caller an error, not
397
+ // a round trip, and certainly not a half-built submission.
398
+ const { fields, submit } = this.#mapDisputeEvidence(evidence);
399
+ const current = await this.#stripe.disputes.retrieve(disputeGatewayId);
400
+ if (!this.#canSubmitEvidence(current)) {
401
+ const dueBy = current.evidence_details?.due_by;
402
+ const pastDue = current.evidence_details?.past_due === true
403
+ ? ' and the evidence deadline has already passed'
404
+ : '';
405
+ const due = typeof dueBy === 'number' ? ` (due by ${new Date(dueBy * 1000).toISOString()})` : '';
406
+ throw new Error(`[payments] Stripe dispute ${disputeGatewayId} will not accept evidence: its status is \`${current.status}\`${pastDue}${due}. Only \`needs_response\` and \`warning_needs_response\` can still be answered — one under review has had its single submission, and a closed one is decided. Read \`canSubmitEvidence\` from \`findDispute()\` before building the case.`);
407
+ }
408
+ const updated = await this.#stripe.disputes.update(disputeGatewayId, {
409
+ evidence: fields,
410
+ submit,
411
+ });
412
+ return this.#mapDispute(updated);
413
+ }
267
414
  // ── Webhooks ─────────────────────────────────────────────────────────────────────────
415
+ /**
416
+ * Whether a delivery to `POST /payments/webhook/:provider` can be authenticated.
417
+ *
418
+ * `parseWebhook` already refuses without the secret — declaring it moves the refusal to boot.
419
+ */
420
+ get webhookVerification() {
421
+ return this.#webhookSecret !== undefined ? 'configured' : 'unconfigured';
422
+ }
268
423
  parseWebhook(rawBody, headers) {
269
424
  if (!this.#webhookSecret) {
270
425
  throw new Error('[payments] Stripe webhook processing requires `STRIPE_WEBHOOK_SECRET` env var.');
@@ -297,15 +452,7 @@ export class StripeDriver {
297
452
  gatewayId: intent.id,
298
453
  provider: this.provider,
299
454
  amount: { amount: intent.amount, currency: intent.currency },
300
- status: intent.status === 'succeeded'
301
- ? 'paid'
302
- : intent.status === 'requires_payment_method' ||
303
- intent.status === 'requires_action' ||
304
- intent.status === 'processing'
305
- ? 'pending'
306
- : intent.status === 'canceled'
307
- ? 'canceled'
308
- : 'failed',
455
+ status: this.#mapIntentStatus(intent.status),
309
456
  payload: intent.metadata !== undefined && Object.keys(intent.metadata).length > 0
310
457
  ? intent.metadata
311
458
  : {},
@@ -318,9 +465,54 @@ export class StripeDriver {
318
465
  if (intent.status === 'succeeded') {
319
466
  result.paidAt = new Date(intent.created * 1000).toISOString();
320
467
  }
468
+ // A Pix/boleto intent is useless to the caller without what the payer has to act on;
469
+ // Stripe puts it under `next_action`. `image_url_png` is a URL, not the base64 PNG
470
+ // `pixQrCodeImage` promises, so it is deliberately not mapped there.
471
+ const pix = intent.next_action?.pix_display_qr_code;
472
+ if (pix?.data !== undefined) {
473
+ result.pixCode = pix.data;
474
+ result.pixCopiaECola = pix.data;
475
+ }
476
+ const hostedUrl = pix?.hosted_instructions_url ??
477
+ intent.next_action?.boleto_display_details?.hosted_voucher_url;
478
+ if (hostedUrl !== null && hostedUrl !== undefined)
479
+ result.hostedUrl = hostedUrl;
321
480
  return result;
322
481
  }
323
- #mapSubscription(subscription) {
482
+ /**
483
+ * A PaymentIntent status → a `BillingStatus`.
484
+ *
485
+ * `requires_capture` is the manual-capture hold: Stripe has the funds reserved on the
486
+ * card and nothing has moved. It used to fall through to `failed` — the same shape of
487
+ * lie in the opposite direction, a live authorization reported as a dead payment — and
488
+ * `requires_confirmation` fell there with it. Neither is a failure; the first is
489
+ * `authorized` and the second is a payment nobody has confirmed yet.
490
+ */
491
+ #mapIntentStatus(status) {
492
+ switch (status) {
493
+ case 'succeeded':
494
+ return 'paid';
495
+ case 'requires_capture':
496
+ return 'authorized';
497
+ case 'requires_payment_method':
498
+ case 'requires_confirmation':
499
+ case 'requires_action':
500
+ case 'processing':
501
+ return 'pending';
502
+ case 'canceled':
503
+ return 'canceled';
504
+ default:
505
+ return 'failed';
506
+ }
507
+ }
508
+ /**
509
+ * A Stripe subscription status → a `SubscriptionStatus`.
510
+ *
511
+ * `paused` used to answer `active`, which entitled a subscriber nobody is billing —
512
+ * Stripe pauses collection precisely so you can stop serving them. It has its own name
513
+ * now: the subscription exists, it will bill again, and it must not grant access today.
514
+ */
515
+ #mapSubscriptionStatus(status) {
324
516
  const statusMap = {
325
517
  trialing: 'trialing',
326
518
  active: 'active',
@@ -329,8 +521,11 @@ export class StripeDriver {
329
521
  canceled: 'canceled',
330
522
  unpaid: 'past_due',
331
523
  incomplete_expired: 'ended',
332
- paused: 'active',
524
+ paused: 'paused',
333
525
  };
526
+ return (status !== undefined ? statusMap[status] : undefined) ?? 'active';
527
+ }
528
+ #mapSubscription(subscription) {
334
529
  return {
335
530
  id: subscription.id,
336
531
  gatewayId: subscription.id,
@@ -338,7 +533,7 @@ export class StripeDriver {
338
533
  customerId: typeof subscription.customer === 'string'
339
534
  ? subscription.customer
340
535
  : subscription.customer.id,
341
- status: statusMap[subscription.status] ?? 'active',
536
+ status: this.#mapSubscriptionStatus(subscription.status),
342
537
  planId: subscription.items.data[0]?.price.id ?? '',
343
538
  ...(subscription.items.data[0]?.price.unit_amount !== null
344
539
  ? {
@@ -361,23 +556,609 @@ export class StripeDriver {
361
556
  };
362
557
  }
363
558
  #mapWebhookEvent(event) {
559
+ const object = event.data.object;
560
+ const normalized = this.#normalizeEvent(event.type, object);
364
561
  return {
365
562
  id: event.id,
366
563
  provider: this.provider,
367
- type: event.type,
368
- createdAt: new Date(event.created * 1000).toISOString(),
369
- data: event.data.object,
564
+ type: normalized.type,
565
+ // Guarded: `new Date(undefined * 1000).toISOString()` throws a bare RangeError, which
566
+ // surfaces as a crash rather than as this package's own rejection. A real Stripe event
567
+ // always carries `created`, so this only fires on a malformed body — but a malformed
568
+ // body should be REFUSED, not turned into a stack trace.
569
+ //
570
+ // Deliberately untested: this spec's mocked Stripe SDK fills `created`, so a test here
571
+ // passes with and without the guard, which measures nothing. Reaching it needs the real
572
+ // SDK's `constructEvent` over a hand-signed body.
573
+ createdAt: Number.isFinite(event.created)
574
+ ? new Date(event.created * 1000).toISOString()
575
+ : new Date().toISOString(),
576
+ data: normalized.data,
370
577
  raw: event,
371
578
  };
372
579
  }
580
+ /**
581
+ * Stripe's own event type → the canonical one, with the payload the processor syncs on.
582
+ *
583
+ * This driver used to pass `event.type` and the raw Stripe object straight through, so
584
+ * `WebhookProcessor` — which switches on the canonical names — recognized nothing Stripe
585
+ * sent and synced nothing: `billing_payments` stayed empty while every webhook was
586
+ * ledgered as processed. A chargeback could not move a row that was never written.
587
+ *
588
+ * A type is only renamed when the canonical payload can actually be built from the
589
+ * object; when it cannot, the event passes through under its Stripe name with the raw
590
+ * object as `data`, which is what the processor does with anything it does not know.
591
+ * That matters because the built-in handlers THROW on a malformed payload, and a throw
592
+ * inside the webhook route is a 500 that Stripe retries forever.
593
+ */
594
+ #normalizeEvent(type, object) {
595
+ const passthrough = { type, data: object };
596
+ const as = (canonical, data) => data === undefined ? passthrough : { type: canonical, data: data };
597
+ switch (type) {
598
+ case 'payment_intent.succeeded':
599
+ return as('payment.succeeded', this.#intentData(object));
600
+ case 'payment_intent.payment_failed':
601
+ return as('payment.failed', this.#intentData(object));
602
+ // Real state changes with no canonical event of their own. `amount_capturable_updated`
603
+ // is the manual-capture authorization — money held, not moved — and there is
604
+ // deliberately no `payment.authorized` event for it to become.
605
+ case 'payment_intent.canceled':
606
+ case 'payment_intent.processing':
607
+ case 'payment_intent.requires_action':
608
+ case 'payment_intent.amount_capturable_updated':
609
+ return as('payment.updated', this.#intentData(object));
610
+ case 'charge.refunded': {
611
+ const charge = object;
612
+ // Stripe fires this for a PARTIAL refund too, and the canonical `payment.refunded`
613
+ // marks the whole payment refunded. Only `refunded: true` — Stripe's own "nothing
614
+ // left" flag — means that; a partial one is an update.
615
+ return as(charge.refunded === true ? 'payment.refunded' : 'payment.updated', this.#chargeData(object));
616
+ }
617
+ // The dispute family. `charge.dispute.created` fires for BOTH a chargeback and an
618
+ // inquiry, and the two are not the same event at all: on an inquiry the card network
619
+ // is asking a question and **no funds are withdrawn**, so calling it
620
+ // `payment.disputed` took a paid payment away over a question. Stripe distinguishes
621
+ // them only by the status prefix — an inquiry's is `warning_*`.
622
+ case 'charge.dispute.created': {
623
+ const dispute = object;
624
+ return typeof dispute.status === 'string' && dispute.status.startsWith('warning_')
625
+ ? as('payment.dispute_warning', this.#disputeWarningData(object))
626
+ : as('payment.disputed', this.#disputeData(object));
627
+ }
628
+ // An early fraud warning is the issuer's TC40/SAFE fraud report, and it arrives
629
+ // before any dispute exists. Stripe's own figure is that ~80% of them become a fraud
630
+ // dispute if you do nothing, and a refund inside the window stops the chargeback
631
+ // being filed at all — which is worth doing even on one you would win, because the
632
+ // chargeback still counts against the ratio that triggers network monitoring.
633
+ case 'radar.early_fraud_warning.created':
634
+ return as('payment.dispute_warning', this.#fraudWarningData(object));
635
+ case 'charge.dispute.closed': {
636
+ const outcome = this.#disputeOutcome(object);
637
+ // Only a status that names an outcome becomes a close. Anything else stays an
638
+ // update rather than inventing a result for money that has not settled.
639
+ return outcome === undefined
640
+ ? as('payment.updated', this.#disputeData(object))
641
+ : as('payment.dispute_closed', this.#disputeClosedData(object, outcome));
642
+ }
643
+ // Funds moving during an open dispute, and evidence being submitted. Neither is a
644
+ // resolution.
645
+ case 'charge.dispute.updated':
646
+ case 'charge.dispute.funds_withdrawn':
647
+ case 'charge.dispute.funds_reinstated':
648
+ return as('payment.updated', this.#disputeData(object));
649
+ case 'checkout.session.completed':
650
+ case 'checkout.session.async_payment_succeeded': {
651
+ const session = object;
652
+ // A completed session is not a paid one: a subscription checkout completes with no
653
+ // payment at all, and a delayed-notification method (boleto, SEPA) completes while
654
+ // the money is still in flight. `payment_status` is the only field that says so.
655
+ return as(session.payment_status === 'paid' ? 'payment.succeeded' : 'payment.updated', this.#sessionData(object));
656
+ }
657
+ case 'checkout.session.async_payment_failed':
658
+ return as('payment.failed', this.#sessionData(object));
659
+ case 'checkout.session.expired':
660
+ return as('payment.updated', this.#sessionData(object));
661
+ case 'customer.subscription.created':
662
+ return as('subscription.created', this.#subscriptionData(object));
663
+ case 'customer.subscription.updated':
664
+ case 'customer.subscription.paused':
665
+ case 'customer.subscription.resumed':
666
+ case 'customer.subscription.trial_will_end':
667
+ return as('subscription.updated', this.#subscriptionData(object));
668
+ case 'customer.subscription.deleted':
669
+ return as('subscription.canceled', this.#subscriptionData(object));
670
+ default:
671
+ // Everything else keeps its Stripe name and its raw object: unknown is ledgered
672
+ // and handed to a registered handler, never dropped.
673
+ return passthrough;
674
+ }
675
+ }
676
+ /** A PaymentIntent → the canonical payment payload, or `undefined` if it is not one. */
677
+ #intentData(object) {
678
+ const intent = object;
679
+ if (typeof intent.id !== 'string' ||
680
+ typeof intent.amount !== 'number' ||
681
+ typeof intent.currency !== 'string') {
682
+ return undefined;
683
+ }
684
+ const metadata = (intent.metadata ?? {});
685
+ return {
686
+ gatewayId: intent.id,
687
+ amount: intent.amount,
688
+ currency: intent.currency,
689
+ ...(typeof intent.customer === 'string' ? { customerId: intent.customer } : {}),
690
+ ...(typeof metadata.external_reference === 'string'
691
+ ? { externalReference: metadata.external_reference }
692
+ : {}),
693
+ };
694
+ }
695
+ /**
696
+ * A Charge → the canonical payment payload, keyed on the **PaymentIntent**.
697
+ *
698
+ * `charge()` returns `pi_…` and every other event here is keyed on it, so a refund
699
+ * ledgered under `ch_…` would write a second row for the same money instead of moving
700
+ * the first. The charge id is only used for a legacy Charges-API charge that has no
701
+ * intent at all.
702
+ */
703
+ #chargeData(object) {
704
+ const charge = object;
705
+ const gatewayId = typeof charge.payment_intent === 'string'
706
+ ? charge.payment_intent
707
+ : typeof charge.id === 'string'
708
+ ? charge.id
709
+ : undefined;
710
+ if (gatewayId === undefined ||
711
+ typeof charge.amount !== 'number' ||
712
+ typeof charge.currency !== 'string') {
713
+ return undefined;
714
+ }
715
+ const metadata = (charge.metadata ?? {});
716
+ return {
717
+ gatewayId,
718
+ amount: charge.amount,
719
+ currency: charge.currency,
720
+ ...(typeof charge.customer === 'string' ? { customerId: charge.customer } : {}),
721
+ ...(typeof metadata.external_reference === 'string'
722
+ ? { externalReference: metadata.external_reference }
723
+ : {}),
724
+ };
725
+ }
726
+ /**
727
+ * A Dispute → the canonical payment payload.
728
+ *
729
+ * `payment_intent` is nullable on the Dispute object (a charge created without one has
730
+ * none), so the charge id is the fallback rather than an assumption. The amount is the
731
+ * DISPUTED amount, which for a partial dispute is less than the payment — the row moves
732
+ * to `disputed` either way, and `event.raw` carries the reason and the evidence deadline.
733
+ */
734
+ #disputeData(object) {
735
+ const dispute = object;
736
+ const gatewayId = typeof dispute.payment_intent === 'string'
737
+ ? dispute.payment_intent
738
+ : typeof dispute.charge === 'string'
739
+ ? dispute.charge
740
+ : undefined;
741
+ if (gatewayId === undefined ||
742
+ typeof dispute.amount !== 'number' ||
743
+ typeof dispute.currency !== 'string') {
744
+ return undefined;
745
+ }
746
+ return { gatewayId, amount: dispute.amount, currency: dispute.currency };
747
+ }
748
+ /**
749
+ * An inquiry (`warning_*`) or the pre-dispute half of the dispute family. Carries the
750
+ * deadline, which is the entire value of the alert: an inquiry left unanswered reads to
751
+ * the issuer as accepting the claim and becomes a chargeback that is probably
752
+ * irreversible.
753
+ */
754
+ #disputeWarningData(object) {
755
+ const base = this.#disputeData(object);
756
+ if (base === undefined)
757
+ return undefined;
758
+ const dispute = object;
759
+ const dueBy = dispute.evidence_details?.due_by;
760
+ return {
761
+ ...base,
762
+ disputeId: dispute.id,
763
+ ...(typeof dispute.reason === 'string' ? { reason: dispute.reason } : {}),
764
+ ...(typeof dueBy === 'number'
765
+ ? { actionableUntil: new Date(dueBy * 1000).toISOString() }
766
+ : {}),
767
+ };
768
+ }
769
+ /**
770
+ * An early fraud warning has no deadline of its own — the window closes when the
771
+ * chargeback is filed, which is the thing you are trying to prevent — so it carries no
772
+ * `actionableUntil`. `actionable` is Stripe's own flag for whether anything can still be
773
+ * done: false once a dispute has arrived or the charge is fully refunded.
774
+ */
775
+ #fraudWarningData(object) {
776
+ const warning = object;
777
+ const gatewayId = typeof warning.payment_intent === 'string'
778
+ ? warning.payment_intent
779
+ : typeof warning.charge === 'string'
780
+ ? warning.charge
781
+ : undefined;
782
+ if (gatewayId === undefined)
783
+ return undefined;
784
+ return {
785
+ gatewayId,
786
+ disputeId: warning.id,
787
+ reason: warning.fraud_type,
788
+ actionable: warning.actionable,
789
+ };
790
+ }
791
+ /**
792
+ * `warning_closed` is an inquiry that sat 120 days without escalating. The networks send
793
+ * no explicit win for one, so it is `expired` rather than `won` — nothing was decided in
794
+ * your favour, the clock simply ran out in the right direction.
795
+ */
796
+ #disputeOutcome(object) {
797
+ const status = object.status;
798
+ if (status === 'won')
799
+ return 'won';
800
+ if (status === 'lost')
801
+ return 'lost';
802
+ if (status === 'warning_closed')
803
+ return 'expired';
804
+ return undefined;
805
+ }
806
+ #disputeClosedData(object, outcome) {
807
+ const base = this.#disputeData(object);
808
+ if (base === undefined)
809
+ return undefined;
810
+ return { ...base, disputeId: object.id, outcome };
811
+ }
812
+ /**
813
+ * A Stripe Dispute → the canonical {@link Dispute}.
814
+ *
815
+ * `amount` is the DISPUTED amount, which on a partial chargeback is less than the
816
+ * payment, and `reason` stays Stripe's own code (`fraudulent`, `product_not_received`, …)
817
+ * because the vocabulary is the network's, not this library's.
818
+ */
819
+ #mapDispute(dispute) {
820
+ const dueBy = dispute.evidence_details?.due_by;
821
+ return {
822
+ id: dispute.id,
823
+ provider: this.provider,
824
+ paymentGatewayId: this.#disputedPaymentId(dispute),
825
+ status: this.#mapDisputeStatus(dispute.status),
826
+ ...(typeof dispute.amount === 'number' && typeof dispute.currency === 'string'
827
+ ? { amount: { amount: dispute.amount, currency: dispute.currency } }
828
+ : {}),
829
+ ...(typeof dispute.reason === 'string' ? { reason: dispute.reason } : {}),
830
+ ...(typeof dueBy === 'number' ? { evidenceDueBy: new Date(dueBy * 1000).toISOString() } : {}),
831
+ canSubmitEvidence: this.#canSubmitEvidence(dispute),
832
+ ...(typeof dispute.created === 'number'
833
+ ? { createdAt: new Date(dispute.created * 1000).toISOString() }
834
+ : {}),
835
+ payload: dispute,
836
+ };
837
+ }
838
+ /**
839
+ * The disputed payment's id — the PaymentIntent, falling back to the charge.
840
+ *
841
+ * Same order as the webhook mappers, and for the same reason: `payment_intent` is what
842
+ * `charge()` returns, but it is nullable (a legacy Charges-API charge has none). Both are
843
+ * expandable, so an expanded object is read for its id rather than stringified.
844
+ */
845
+ #disputedPaymentId(dispute) {
846
+ const { payment_intent: intent, charge } = dispute;
847
+ if (typeof intent === 'string')
848
+ return intent;
849
+ if (intent !== null && typeof intent === 'object')
850
+ return intent.id;
851
+ if (typeof charge === 'string')
852
+ return charge;
853
+ if (charge !== null && typeof charge === 'object')
854
+ return charge.id;
855
+ return '';
856
+ }
857
+ /**
858
+ * A Stripe dispute status → the canonical {@link DisputeStatus}.
859
+ *
860
+ * The `warning_*` statuses are inquiries: the network is asking a question and **no
861
+ * funds have been withdrawn**, so they are `warning` and not `open`/`under_review` —
862
+ * the same money line the webhook mapping draws. `warning_closed` is an inquiry that
863
+ * lapsed without escalating, which is `expired` rather than `won`: nothing was decided
864
+ * in your favour, the clock simply ran out the right way. `prevented` — a dispute
865
+ * stopped before it became a formal chargeback — is `canceled`, because the money never
866
+ * left.
867
+ *
868
+ * The parameter is a plain `string`, not `Stripe.Dispute.Status`: `prevented` is in
869
+ * Stripe's current API reference and NOT in the SDK's union at 18.5.0, so typing this
870
+ * against the SDK would make the branch that handles it unreachable to the compiler
871
+ * while the API keeps sending it.
872
+ */
873
+ #mapDisputeStatus(status) {
874
+ switch (status) {
875
+ case 'warning_needs_response':
876
+ case 'warning_under_review':
877
+ return 'warning';
878
+ case 'warning_closed':
879
+ return 'expired';
880
+ case 'needs_response':
881
+ return 'open';
882
+ case 'under_review':
883
+ return 'under_review';
884
+ case 'won':
885
+ return 'won';
886
+ case 'lost':
887
+ return 'lost';
888
+ case 'prevented':
889
+ return 'canceled';
890
+ default:
891
+ // A status Stripe adds after this release. `open` is the reading that keeps it in
892
+ // front of an operator, and `payload` still carries Stripe's own word for it.
893
+ return 'open';
894
+ }
895
+ }
896
+ /**
897
+ * Whether Stripe will still take evidence for this dispute.
898
+ *
899
+ * Read from the status and `evidence_details.past_due`, never inferred from one of them
900
+ * alone: past the deadline the dispute is lost by default and the API rejects the
901
+ * update, and a dispute already `under_review` has spent its one submission. `won`,
902
+ * `lost`, `prevented` and `warning_closed` are over. Some disputes arrive already
903
+ * closed — a network that forbids contesting the reason at all, or a charge Stripe
904
+ * refunded first — and those must read `false` here so the caller learns it before
905
+ * building a case rather than at the API error.
906
+ */
907
+ #canSubmitEvidence(dispute) {
908
+ if (dispute.evidence_details?.past_due === true)
909
+ return false;
910
+ return dispute.status === 'needs_response' || dispute.status === 'warning_needs_response';
911
+ }
912
+ /**
913
+ * A {@link DisputeEvidence} → Stripe's `evidence` hash, or an error naming what cannot
914
+ * be carried.
915
+ *
916
+ * The rule is that nothing is dropped. Where the shared shape and Stripe's fields
917
+ * disagree — a URL against a File upload id, a count of prior payments against Visa's
918
+ * demand for two specific charges — this throws and says what to send instead, because a
919
+ * caller who believes a receipt was submitted and finds out at the outcome has been told
920
+ * a lie by this library.
921
+ */
922
+ #mapDisputeEvidence(evidence) {
923
+ if (evidence.termsAcceptedAt !== undefined) {
924
+ throw new Error('[payments] Stripe has no field for WHEN the customer accepted your terms. Its ' +
925
+ 'nearest homes are the `refund_policy` / `cancellation_policy` documents with the ' +
926
+ 'matching `*_policy_disclosure` text fields, which are where "how and when the ' +
927
+ 'customer was shown this" belongs — pass the policy as a `DisputeDocument` and the ' +
928
+ 'disclosure through `metadata`.');
929
+ }
930
+ const fields = {};
931
+ /** Every write goes through here, so two sources cannot quietly overwrite each other. */
932
+ const put = (field, value, source) => {
933
+ if (fields[field] !== undefined) {
934
+ throw new Error(`[payments] Stripe dispute evidence field \`${field}\` was set twice — by ${source} and by \`metadata.${field}\`. Pass it once; the driver will not pick a winner.`);
935
+ }
936
+ fields[field] = value;
937
+ };
938
+ if (evidence.explanation !== undefined) {
939
+ put('uncategorized_text', evidence.explanation, '`explanation`');
940
+ }
941
+ if (evidence.shippingCarrier !== undefined) {
942
+ put('shipping_carrier', evidence.shippingCarrier, '`shippingCarrier`');
943
+ }
944
+ if (evidence.shippingTrackingNumber !== undefined) {
945
+ put('shipping_tracking_number', evidence.shippingTrackingNumber, '`shippingTrackingNumber`');
946
+ }
947
+ if (evidence.shippingDate !== undefined) {
948
+ put('shipping_date', evidence.shippingDate, '`shippingDate`');
949
+ }
950
+ if (evidence.serviceDate !== undefined) {
951
+ put('service_date', evidence.serviceDate, '`serviceDate`');
952
+ }
953
+ if (evidence.customerName !== undefined) {
954
+ put('customer_name', evidence.customerName, '`customerName`');
955
+ }
956
+ if (evidence.customerEmail !== undefined) {
957
+ put('customer_email_address', evidence.customerEmail, '`customerEmail`');
958
+ }
959
+ if (evidence.customerIpAddress !== undefined) {
960
+ put('customer_purchase_ip', evidence.customerIpAddress, '`customerIpAddress`');
961
+ }
962
+ for (const document of evidence.documents ?? []) {
963
+ const field = STRIPE_DOCUMENT_FIELDS[document.kind];
964
+ put(field, document.id, `\`documents\` (${document.kind})`);
965
+ }
966
+ if (evidence.priorUndisputedPayments !== undefined) {
967
+ const prior = evidence.priorUndisputedPayments;
968
+ // Stripe's own requirement, not a house rule: Visa Compelling Evidence 3.0 is built
969
+ // around exactly two prior transactions, and Stripe rejects any other count. Failing
970
+ // here costs nothing; failing at the API spends the dispute's single submission.
971
+ if (prior.length !== 2) {
972
+ throw new Error(`[payments] Visa Compelling Evidence 3.0 takes exactly TWO prior undisputed transactions, and got ${prior.length}. Stripe also only accepts them on a dispute it has already marked eligible — check \`enhanced_eligibility_types\` on the dispute payload before building the case.`);
973
+ }
974
+ put('enhanced_evidence', {
975
+ visa_compelling_evidence_3: {
976
+ prior_undisputed_transactions: prior.map((transaction) => ({
977
+ charge: transaction.paymentGatewayId,
978
+ ...(transaction.customerAccountId !== undefined
979
+ ? { customer_account_id: transaction.customerAccountId }
980
+ : {}),
981
+ ...(transaction.customerDeviceId !== undefined
982
+ ? { customer_device_id: transaction.customerDeviceId }
983
+ : {}),
984
+ ...(transaction.customerIpAddress !== undefined
985
+ ? { customer_purchase_ip: transaction.customerIpAddress }
986
+ : {}),
987
+ })),
988
+ },
989
+ }, '`priorUndisputedPayments`');
990
+ }
991
+ // `metadata` is the escape hatch the shared type documents, and here it is Stripe's own
992
+ // field names — the only way to reach `product_description`, the `*_policy` files or
993
+ // Visa CE 3.0 without inventing fields on a type eighteen drivers share.
994
+ let submit = true;
995
+ for (const [key, value] of Object.entries(evidence.metadata ?? {})) {
996
+ if (key === 'submit') {
997
+ if (typeof value !== 'boolean') {
998
+ throw new Error('[payments] `metadata.submit` on Stripe dispute evidence must be a boolean: ' +
999
+ '`true` (the default) sends the response to the issuer and is final, `false` ' +
1000
+ 'stages a draft on the dispute.');
1001
+ }
1002
+ submit = value;
1003
+ continue;
1004
+ }
1005
+ if (key === 'enhanced_evidence') {
1006
+ if (typeof value !== 'object' || value === null) {
1007
+ throw new Error("[payments] `metadata.enhanced_evidence` must be Stripe's enhanced evidence " +
1008
+ 'object (`visa_compelling_evidence_3`, `visa_compliance`, ' +
1009
+ '`mastercard_compliance`).');
1010
+ }
1011
+ put('enhanced_evidence', value, '`metadata.enhanced_evidence`');
1012
+ continue;
1013
+ }
1014
+ if (!STRIPE_EVIDENCE_FIELDS.has(key)) {
1015
+ throw new Error(`[payments] \`${key}\` is not a Stripe dispute evidence field, and the driver will not send it — an unrecognized key fails the whole submission at the API. Use Stripe's own field names (https://docs.stripe.com/api/disputes/update), or \`explanation\` for free text.`);
1016
+ }
1017
+ if (typeof value !== 'string') {
1018
+ throw new Error(`[payments] Stripe dispute evidence field \`${key}\` must be a string; got ${typeof value}.`);
1019
+ }
1020
+ if (STRIPE_EVIDENCE_FILE_FIELDS.has(key) && !value.startsWith('file_')) {
1021
+ throw new Error(`[payments] Stripe's \`${key}\` evidence field takes a File upload id (\`file_…\`, created with purpose \`dispute_evidence\`), not \`${value}\`. Upload the document first and pass the id it returns.`);
1022
+ }
1023
+ put(key, value, '`metadata`');
1024
+ }
1025
+ if (Object.keys(fields).length === 0) {
1026
+ throw new Error('[payments] Nothing to submit: the `DisputeEvidence` carried no field Stripe ' +
1027
+ 'accepts. Stripe takes ONE submission per dispute and forwards it to the issuer ' +
1028
+ 'immediately — an empty one spends it.');
1029
+ }
1030
+ return { fields: fields, submit };
1031
+ }
1032
+ /** A Checkout Session → the canonical payment payload, keyed on its PaymentIntent. */
1033
+ #sessionData(object) {
1034
+ const session = object;
1035
+ const gatewayId = typeof session.payment_intent === 'string' ? session.payment_intent : undefined;
1036
+ if (gatewayId === undefined ||
1037
+ typeof session.amount_total !== 'number' ||
1038
+ typeof session.currency !== 'string') {
1039
+ return undefined;
1040
+ }
1041
+ const metadata = (session.metadata ?? {});
1042
+ return {
1043
+ gatewayId,
1044
+ amount: session.amount_total,
1045
+ currency: session.currency,
1046
+ ...(typeof session.customer === 'string' ? { customerId: session.customer } : {}),
1047
+ ...(typeof session.subscription === 'string' ? { subscriptionId: session.subscription } : {}),
1048
+ ...(typeof metadata.external_reference === 'string'
1049
+ ? { externalReference: metadata.external_reference }
1050
+ : typeof session.client_reference_id === 'string'
1051
+ ? { externalReference: session.client_reference_id }
1052
+ : {}),
1053
+ };
1054
+ }
1055
+ /** A Subscription → the canonical subscription payload. */
1056
+ #subscriptionData(object) {
1057
+ const subscription = object;
1058
+ const customerId = typeof subscription.customer === 'string'
1059
+ ? subscription.customer
1060
+ : subscription.customer?.id;
1061
+ if (typeof subscription.id !== 'string' || typeof customerId !== 'string')
1062
+ return undefined;
1063
+ const planId = subscription.items?.data?.[0]?.price?.id;
1064
+ return {
1065
+ gatewayId: subscription.id,
1066
+ customerId,
1067
+ status: this.#mapSubscriptionStatus(subscription.status),
1068
+ ...(typeof planId === 'string' ? { planId } : {}),
1069
+ ...(typeof subscription.trial_end === 'number'
1070
+ ? { trialEndsAt: new Date(subscription.trial_end * 1000).toISOString() }
1071
+ : {}),
1072
+ ...(typeof subscription.ended_at === 'number'
1073
+ ? { endsAt: new Date(subscription.ended_at * 1000).toISOString() }
1074
+ : {}),
1075
+ };
1076
+ }
1077
+ /**
1078
+ * Canonical method → the Stripe `payment_method_types` entry that produces it. An
1079
+ * unnamed (or unmappable) method leaves the intent on the account's dynamic payment
1080
+ * methods, which is Stripe's own default rather than a silent substitution.
1081
+ */
1082
+ #mapMethodToStripe(method) {
1083
+ switch (method) {
1084
+ case 'pix':
1085
+ return 'pix';
1086
+ case 'boleto':
1087
+ return 'boleto';
1088
+ case 'credit_card':
1089
+ return 'card';
1090
+ default:
1091
+ return undefined;
1092
+ }
1093
+ }
1094
+ /**
1095
+ * Stripe deduplicates on the `Idempotency-Key` request header, which the SDK takes as a
1096
+ * request option — a key sent in the body retries into a second charge.
1097
+ */
1098
+ #requestOptions(idempotencyKey) {
1099
+ return idempotencyKey !== undefined ? { idempotencyKey } : undefined;
1100
+ }
1101
+ /**
1102
+ * A Stripe `payment_method_types` entry → the canonical {@link PaymentMethodType}.
1103
+ *
1104
+ * By category, never by brand: Stripe adds a local method most quarters, and a driver
1105
+ * that enumerated iDEAL, BLIK, TWINT and the rest would go stale between releases while
1106
+ * `PaymentMethodType` — a closed union — could never keep up. So SEPA and ACH are one
1107
+ * answer (`bank_debit`, pulled from an account), iDEAL and Bancontact another
1108
+ * (`bank_transfer`, pushed from a bank), and the brand stays readable on
1109
+ * `payment.payload`.
1110
+ *
1111
+ * The type is read off `payment_method_types[0]`, which is the list of methods the
1112
+ * intent ALLOWS. With one entry — what this driver sends whenever the charge names a
1113
+ * `method` — it is exact; with the account's dynamic payment methods it is Stripe's own
1114
+ * ordering, and the settled method is only known once `latest_charge` exists.
1115
+ */
373
1116
  #mapMethod(method) {
374
1117
  switch (method) {
375
1118
  case 'card':
1119
+ case 'card_present':
376
1120
  return 'card';
377
1121
  case 'pix':
378
1122
  return 'pix';
379
1123
  case 'boleto':
380
1124
  return 'boleto';
1125
+ // Pulled from an account you hold a mandate on.
1126
+ case 'sepa_debit':
1127
+ case 'us_bank_account':
1128
+ case 'acss_debit':
1129
+ case 'bacs_debit':
1130
+ case 'au_becs_debit':
1131
+ return 'bank_debit';
1132
+ // Pushed from the payer's own bank, in their own banking app.
1133
+ case 'ideal':
1134
+ case 'bancontact':
1135
+ case 'eps':
1136
+ case 'p24':
1137
+ case 'blik':
1138
+ case 'multibanco':
1139
+ case 'sofort':
1140
+ case 'customer_balance':
1141
+ return 'bank_transfer';
1142
+ // Stored-balance and device wallets.
1143
+ case 'link':
1144
+ case 'paypal':
1145
+ case 'wechat_pay':
1146
+ case 'alipay':
1147
+ case 'cashapp':
1148
+ case 'revolut_pay':
1149
+ case 'amazon_pay':
1150
+ case 'twint':
1151
+ return 'wallet';
1152
+ case 'klarna':
1153
+ case 'afterpay_clearpay':
1154
+ case 'affirm':
1155
+ case 'zip':
1156
+ return 'bnpl';
1157
+ // Paid in cash at a counter against a printed reference.
1158
+ case 'oxxo':
1159
+ case 'konbini':
1160
+ case 'paysafecard':
1161
+ return 'voucher';
381
1162
  default:
382
1163
  return 'unknown';
383
1164
  }