@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,812 @@
1
+ import { createHmac } from 'node:crypto';
2
+ import { publishPaymentDiagnostics, publishRefundDiagnostics, publishSubscriptionDiagnostics, } from '../diagnostics.js';
3
+ import { headerValue, httpRequest, isNotFound } from '../http.js';
4
+ import { emitInvoiceIfRequested } from '../invoice/emit_invoice.js';
5
+ import { formatDecimal, fromDecimal } from '../money.js';
6
+ import { safeCompare } from '../webhook_security.js';
7
+ import { requireCredential, requireCurrency } from './shared.js';
8
+ /**
9
+ * Canonical method **category** → the Mollie method ids in it.
10
+ *
11
+ * Mollie's `method` parameter takes a single id *or an array of ids*, and an array is
12
+ * exactly what a category is: `bank_transfer` is not one brand, it is iDEAL in the
13
+ * Netherlands, Bancontact in Belgium, Multibanco in Portugal and eleven others. Sending
14
+ * the whole set restricts the hosted page to that category and lets Mollie show the
15
+ * buyer the ones their country and your profile actually enable.
16
+ *
17
+ * To pin one brand — "iDEAL, nothing else" — pass `metadata.mollieMethod` with the exact
18
+ * Mollie id; that is the gateway's own field, which is where a brand belongs.
19
+ */
20
+ const MOLLIE_METHOD_CATEGORIES = {
21
+ credit_card: ['creditcard'],
22
+ // Push-from-your-bank. Mollie's own docs group these as its "bank-based" methods.
23
+ bank_transfer: [
24
+ 'ideal',
25
+ 'bancontact',
26
+ 'banktransfer',
27
+ 'belfius',
28
+ 'bizum',
29
+ 'blik',
30
+ 'eps',
31
+ 'kbc',
32
+ 'mbway',
33
+ 'mobilepay',
34
+ 'multibanco',
35
+ 'mybank',
36
+ 'paybybank',
37
+ 'przelewy24',
38
+ 'satispay',
39
+ 'swish',
40
+ 'trustly',
41
+ 'twint',
42
+ 'vipps',
43
+ 'bancomatpay',
44
+ ],
45
+ // Pull-from-your-account mandates: SEPA Direct Debit and its UK equivalent, Bacs.
46
+ bank_debit: ['directdebit', 'bacs'],
47
+ wallet: ['paypal', 'applepay', 'googlepay'],
48
+ bnpl: ['klarna', 'in3', 'riverty', 'billie', 'billink', 'alma'],
49
+ // Stored-value paper and plastic: meal/eco vouchers, gift cards, paysafecard.
50
+ voucher: ['voucher', 'giftcard', 'paysafecard'],
51
+ };
52
+ /**
53
+ * Mollie driver — European gateway (api.mollie.com/v2), Bearer API key, multi-currency.
54
+ *
55
+ * Two things about Mollie shape this driver more than anything else:
56
+ *
57
+ * 1. **Money is a decimal string** (`{ "currency": "EUR", "value": "10.00" }`). The whole
58
+ * package works in the currency's smallest unit, so the conversion happens once on the
59
+ * way out (`#toMollieAmount`) and once on the way in (`#fromMollieAmount`).
60
+ * 2. **The classic webhook is a bare id** — `id=tr_xxx`, form-encoded, unsigned. Mollie
61
+ * deliberately leaves the status out so a forged call cannot tell you a payment was
62
+ * paid; you learn what happened, and that the call is genuine, by fetching the payment
63
+ * with your API key. `parseWebhook` is async here for exactly that reason.
64
+ */
65
+ export class MollieDriver {
66
+ provider = 'mollie';
67
+ /**
68
+ * What `charge()` can genuinely ask Mollie for, now that the contract names **categories**
69
+ * instead of brands. Mollie's catalogue is almost entirely local European methods and
70
+ * none of them had a name here, so every one of iDEAL, Bancontact, SEPA Direct Debit,
71
+ * Klarna, PayPal, Apple Pay, EPS, Przelewy24, BLIK, TWINT, MB WAY, Multibanco, Trustly,
72
+ * paysafecard and the vouchers was unroutable. Each now falls into a category, and the
73
+ * category goes out as Mollie's own `method` array (see {@link MollieDriver.#mapMethod}).
74
+ *
75
+ * `debit_card` is still absent, and re-checking did not change that: Mollie folds debit
76
+ * cards (Maestro, V PAY) into the single `creditcard` method id and exposes no separate
77
+ * debit id, so a `debit_card` route could only be a `creditcard` charge wearing another
78
+ * name. `pix` and `boleto` are not European methods and Mollie has neither.
79
+ */
80
+ supportedMethods = [
81
+ 'credit_card',
82
+ 'bank_transfer',
83
+ 'bank_debit',
84
+ 'wallet',
85
+ 'bnpl',
86
+ 'voucher',
87
+ 'undefined',
88
+ ];
89
+ /** No invoices: Mollie's Invoices API returns Mollie's own invoices to *you*, not yours. */
90
+ capabilities = { refunds: true, invoices: false, subscriptions: true };
91
+ #baseUrl = 'https://api.mollie.com/v2';
92
+ #apiKey;
93
+ #currency;
94
+ #webhookSecret;
95
+ #invoiceCtx;
96
+ /**
97
+ * Subscription id → customer id. Every Mollie subscription endpoint is nested under a
98
+ * customer, but the driver contract passes only the subscription id; this remembers the
99
+ * pairing for subscriptions this process created, so the common path costs no lookup.
100
+ */
101
+ #subscriptionCustomers = new Map();
102
+ constructor(ctx, config) {
103
+ this.#invoiceCtx = ctx;
104
+ this.#apiKey = requireCredential({
105
+ driver: 'mollie',
106
+ option: 'apiKey',
107
+ env: 'MOLLIE_API_KEY',
108
+ value: config.apiKey,
109
+ });
110
+ this.#currency = requireCurrency('mollie', config.currency);
111
+ this.#webhookSecret = config.webhookSecret ?? process.env.MOLLIE_WEBHOOK_SECRET;
112
+ }
113
+ // ── Customers ────────────────────────────────────────────────────────────────────────
114
+ async createCustomer(input) {
115
+ const data = await this.#request('/customers', {
116
+ method: 'POST',
117
+ body: {
118
+ ...(input.name !== undefined ? { name: input.name } : {}),
119
+ ...(input.email !== undefined ? { email: input.email } : {}),
120
+ ...this.#customerMetadata(input),
121
+ },
122
+ ...this.#idempotency(input.idempotencyKey),
123
+ });
124
+ return this.#mapCustomer(data);
125
+ }
126
+ async findCustomer(customerId) {
127
+ try {
128
+ return this.#mapCustomer(await this.#request(`/customers/${customerId}`));
129
+ }
130
+ catch (error) {
131
+ if (isNotFound(error))
132
+ return null;
133
+ throw error;
134
+ }
135
+ }
136
+ async updateCustomer(customerId, input) {
137
+ const data = await this.#request(`/customers/${customerId}`, {
138
+ method: 'PATCH',
139
+ body: {
140
+ ...(input.name !== undefined ? { name: input.name } : {}),
141
+ ...(input.email !== undefined ? { email: input.email } : {}),
142
+ ...this.#customerMetadata(input),
143
+ },
144
+ });
145
+ return this.#mapCustomer(data);
146
+ }
147
+ // ── Payments ─────────────────────────────────────────────────────────────────────────
148
+ async charge(input) {
149
+ if (input.split !== undefined && input.split.length > 0) {
150
+ throw new Error('[payments] Mollie does not take a percent/fixed wallet split on a payment. ' +
151
+ 'Its equivalent (Mollie Connect routes) splits to organization ids with absolute ' +
152
+ 'amounts only, which `split` cannot express — create the routes through the Mollie API.');
153
+ }
154
+ // A stored card at Mollie is a *mandate*, and charging one is `sequenceType: recurring`.
155
+ const mandateId = input.card?.token ?? input.paymentMethodId;
156
+ const recurring = mandateId !== undefined;
157
+ const transport = this.#transportOptions(input.metadata);
158
+ // Mollie requires `redirectUrl` on every payment the shopper completes themselves; a
159
+ // recurring charge against a mandate is the one case with nobody to redirect.
160
+ if (!recurring && transport.redirectUrl === undefined) {
161
+ throw new Error('[payments] Mollie requires a redirect URL on a customer-facing payment. ' +
162
+ 'Pass `metadata.redirectUrl` on the charge, or use `createCheckout({ successUrl })`.');
163
+ }
164
+ if (recurring && input.customerId === undefined) {
165
+ throw new Error('[payments] Mollie needs the customer that owns the mandate. Pass `customerId` ' +
166
+ 'alongside the mandate id when charging a stored card.');
167
+ }
168
+ const method = this.#mapMethod(input.method, input.metadata);
169
+ const body = {
170
+ amount: this.#toMollieAmount(input.amount, input.currency),
171
+ // Mollie makes `description` mandatory — it is what the payer sees on their statement.
172
+ description: input.description ?? 'Payment',
173
+ ...(transport.redirectUrl !== undefined ? { redirectUrl: transport.redirectUrl } : {}),
174
+ ...(transport.cancelUrl !== undefined ? { cancelUrl: transport.cancelUrl } : {}),
175
+ ...(transport.webhookUrl !== undefined ? { webhookUrl: transport.webhookUrl } : {}),
176
+ ...(method !== undefined ? { method } : {}),
177
+ ...(input.customerId !== undefined ? { customerId: input.customerId } : {}),
178
+ ...(mandateId !== undefined ? { mandateId, sequenceType: 'recurring' } : {}),
179
+ ...this.#metadataBody(transport.rest, input.externalReference),
180
+ };
181
+ const data = await this.#request('/payments', {
182
+ method: 'POST',
183
+ body,
184
+ ...this.#idempotency(input.idempotencyKey),
185
+ });
186
+ const payment = this.#mapPayment(data);
187
+ await emitInvoiceIfRequested(this.#invoiceCtx, input, payment, this);
188
+ publishPaymentDiagnostics(payment);
189
+ return payment;
190
+ }
191
+ async findPayment(gatewayId) {
192
+ try {
193
+ return this.#mapPayment(await this.#request(`/payments/${gatewayId}`));
194
+ }
195
+ catch (error) {
196
+ if (isNotFound(error))
197
+ return null;
198
+ throw error;
199
+ }
200
+ }
201
+ /**
202
+ * `options.idempotencyKey` goes out as Mollie's `Idempotency-Key` header — the only
203
+ * thing Mollie deduplicates on, and the difference between a retried refund and a second
204
+ * one. Mollie keeps a key for one hour; after that the same key refunds again.
205
+ */
206
+ async refund(paymentGatewayId, amount, options) {
207
+ // Mollie makes `amount` mandatory on a refund and the amount must carry the *payment's*
208
+ // currency, which a charge may have overridden — so read it off the payment rather than
209
+ // assuming the driver's configured one.
210
+ const payment = await this.#request(`/payments/${paymentGatewayId}`);
211
+ const value = amount !== undefined
212
+ ? {
213
+ currency: payment.amount.currency,
214
+ value: formatDecimal(amount, payment.amount.currency),
215
+ }
216
+ : payment.amount;
217
+ const data = await this.#request(`/payments/${paymentGatewayId}/refunds`, { method: 'POST', body: { amount: value }, ...this.#idempotency(options?.idempotencyKey) });
218
+ const refund = {
219
+ id: data.id,
220
+ gatewayId: data.id,
221
+ provider: this.provider,
222
+ amount: this.#fromMollieAmount(data.amount),
223
+ status: data.status === 'refunded'
224
+ ? 'succeeded'
225
+ : data.status === 'failed' || data.status === 'canceled'
226
+ ? 'failed'
227
+ : 'pending',
228
+ createdAt: data.createdAt,
229
+ };
230
+ publishRefundDiagnostics(refund);
231
+ return refund;
232
+ }
233
+ // ── Checkout ─────────────────────────────────────────────────────────────────────────
234
+ async createCheckout(input) {
235
+ if (input.planId !== undefined || input.trialDays !== undefined) {
236
+ throw new Error('[payments] Mollie has no subscription checkout: a subscription needs a mandate, ' +
237
+ 'which the shopper grants by completing a first payment. Check out the first ' +
238
+ 'payment, then call `createSubscription` once the mandate is valid.');
239
+ }
240
+ const transport = this.#transportOptions(input.metadata);
241
+ const data = await this.#request('/payments', {
242
+ method: 'POST',
243
+ body: {
244
+ amount: this.#toMollieAmount(input.amount, input.currency),
245
+ description: input.description ?? 'Checkout',
246
+ redirectUrl: input.successUrl,
247
+ ...(input.cancelUrl !== undefined ? { cancelUrl: input.cancelUrl } : {}),
248
+ ...(transport.webhookUrl !== undefined ? { webhookUrl: transport.webhookUrl } : {}),
249
+ ...(input.customerId !== undefined ? { customerId: input.customerId } : {}),
250
+ ...this.#metadataBody(transport.rest, input.externalReference),
251
+ },
252
+ ...this.#idempotency(input.idempotencyKey),
253
+ });
254
+ const url = data._links?.checkout?.href;
255
+ if (url === undefined) {
256
+ // An empty redirect URL fails at the browser, long after the caller could react.
257
+ throw new Error(`[payments] Mollie returned payment ${data.id} without a checkout link. That happens when the payment cannot be paid by the shopper (e.g. a recurring sequence) — use \`charge()\` for those.`);
258
+ }
259
+ return {
260
+ id: data.id,
261
+ gatewayId: data.id,
262
+ provider: this.provider,
263
+ url,
264
+ status: data.status === 'paid' ? 'complete' : data.status === 'expired' ? 'expired' : 'open',
265
+ amount: this.#fromMollieAmount(data.amount),
266
+ ...(data.customerId !== undefined ? { customerId: data.customerId } : {}),
267
+ };
268
+ }
269
+ // ── Subscriptions ────────────────────────────────────────────────────────────────────
270
+ async createSubscription(input) {
271
+ if (input.amount === undefined) {
272
+ throw new Error('[payments] Mollie requires an `amount` on every subscription.');
273
+ }
274
+ if (input.trialDays !== undefined) {
275
+ throw new Error('[payments] Mollie has no trial period on a subscription — it would have to be ' +
276
+ 'faked as a later first charge, and nothing at the gateway would call it a trial. ' +
277
+ 'Pass `startDate` for the first charge date instead.');
278
+ }
279
+ const transport = this.#transportOptions(input.metadata);
280
+ const body = {
281
+ amount: this.#toMollieAmount(input.amount, undefined),
282
+ interval: this.#mapCycle(input.cycle),
283
+ // Mollie requires a description and shows it on the payer's statement; it must be
284
+ // unique among a customer's active subscriptions, so the plan id is a good default.
285
+ description: input.description ?? input.planId,
286
+ ...(input.startDate !== undefined ? { startDate: input.startDate } : {}),
287
+ ...(input.card?.token !== undefined ? { mandateId: input.card.token } : {}),
288
+ ...(transport.webhookUrl !== undefined ? { webhookUrl: transport.webhookUrl } : {}),
289
+ // Mollie has no plan resource, so the plan id survives only as metadata — and
290
+ // `#mapSubscription` reads it back out of there.
291
+ ...this.#metadataBody({ ...transport.rest, planId: input.planId }, input.externalReference),
292
+ };
293
+ const data = await this.#request(`/customers/${input.customerId}/subscriptions`, { method: 'POST', body, ...this.#idempotency(input.idempotencyKey) });
294
+ const subscription = this.#mapSubscription(data);
295
+ publishSubscriptionDiagnostics(subscription, 'subscription.created');
296
+ return subscription;
297
+ }
298
+ async cancelSubscription(subscriptionGatewayId, _options) {
299
+ // Mollie cancels the schedule outright; there is no period-end flag to honor.
300
+ const { customerId, subscriptionId } = await this.#resolveSubscription(subscriptionGatewayId);
301
+ const data = await this.#request(`/customers/${customerId}/subscriptions/${subscriptionId}`, { method: 'DELETE' });
302
+ const subscription = this.#mapSubscription(data);
303
+ publishSubscriptionDiagnostics(subscription, 'subscription.canceled');
304
+ return subscription;
305
+ }
306
+ /**
307
+ * `input.idempotencyKey` is REFUSED, not ignored. Mollie's own words: "All `POST`
308
+ * endpoints accept idempotency keys. Sending idempotency keys for `GET`, `PATCH`, or
309
+ * `DELETE` requests is not necessary since these API requests are repeatable by nature."
310
+ * Updating a subscription is a `PATCH`, so there is no key to send and accepting one
311
+ * would promise a deduplication Mollie never performs.
312
+ */
313
+ async updateSubscription(subscriptionGatewayId, input) {
314
+ if (input.idempotencyKey !== undefined) {
315
+ throw new Error('[payments] Mollie accepts `Idempotency-Key` on POST requests only, and updating a ' +
316
+ 'subscription is a PATCH — so `idempotencyKey` cannot be honoured on ' +
317
+ 'updateSubscription(). The PATCH is repeatable by nature (it sets fields to the ' +
318
+ 'values you pass), so drop the key rather than relying on one Mollie ignores.');
319
+ }
320
+ const { customerId, subscriptionId } = await this.#resolveSubscription(subscriptionGatewayId);
321
+ const data = await this.#request(`/customers/${customerId}/subscriptions/${subscriptionId}`, {
322
+ method: 'PATCH',
323
+ body: {
324
+ ...(input.amount !== undefined
325
+ ? { amount: this.#toMollieAmount(input.amount, undefined) }
326
+ : {}),
327
+ ...(input.description !== undefined ? { description: input.description } : {}),
328
+ ...(input.metadata !== undefined ? { metadata: input.metadata } : {}),
329
+ },
330
+ });
331
+ return this.#mapSubscription(data);
332
+ }
333
+ async findSubscription(gatewayId) {
334
+ try {
335
+ const { customerId, subscriptionId } = await this.#resolveSubscription(gatewayId);
336
+ return this.#mapSubscription(await this.#request(`/customers/${customerId}/subscriptions/${subscriptionId}`));
337
+ }
338
+ catch (error) {
339
+ if (isNotFound(error))
340
+ return null;
341
+ throw error;
342
+ }
343
+ }
344
+ // ── Invoices ─────────────────────────────────────────────────────────────────────────
345
+ async listInvoices(_customerId) {
346
+ throw new Error('[payments] Mollie has no customer invoices to list: its Invoices API returns the ' +
347
+ 'monthly invoices Mollie issues to *you* for its fees, not documents you issue to ' +
348
+ 'your customers. Configure an `invoice` provider and pass `invoice: true` on the charge.');
349
+ }
350
+ // ── Webhooks ─────────────────────────────────────────────────────────────────────────
351
+ /**
352
+ * Mollie's classic webhook carries one thing: `id=tr_xxx`, form-encoded and unsigned.
353
+ * That is deliberate — because the body never says "paid", a forged call cannot make you
354
+ * think a payment succeeded. **Authenticity comes from the fetch, not from the request**:
355
+ * the driver reads the payment back with your API key, and what that authenticated call
356
+ * returns is the event. Nothing here trusts a word of the request body beyond the id.
357
+ *
358
+ * Which is why this is async. A synchronous parse could only report "payment tr_x
359
+ * changed", so the mounted route ledgered a row and never marked the payment paid — the
360
+ * money arriving and the app never finding out.
361
+ *
362
+ * A fetch failure propagates on purpose: the route answers 400 and Mollie retries, which
363
+ * is the right outcome for a payment nobody could confirm.
364
+ *
365
+ * When `webhookSecret` is configured the driver is talking to Mollie's *next-gen*
366
+ * webhooks instead, which send a JSON event and sign it with `X-Mollie-Signature`
367
+ * (`sha256=<hex hmac of the raw body>`) — then verification is mandatory, fail-closed.
368
+ * Those still get fetched: a signature proves who sent the event, not what the payment
369
+ * is worth, and the built-in billing sync needs the amount.
370
+ */
371
+ /**
372
+ * Whether a delivery to `POST /payments/webhook/:provider` can be authenticated.
373
+ *
374
+ * Next-gen webhooks are signed, so a configured secret is `'configured'`. Without one the
375
+ * driver is on the CLASSIC flow, and that flow is `'unsupported'` rather than
376
+ * `'unconfigured'` — the distinction matters, because `'unconfigured'` refuses to boot.
377
+ *
378
+ * There is no credential to configure on the classic flow and nothing insecure about not
379
+ * having one: the request carries only `id=tr_xxx`, and the driver authenticates by
380
+ * reading that payment back with your API key (see `parseWebhook` below). Reporting it as
381
+ * a missing credential would make the supported, safe configuration fail at boot and push
382
+ * apps into `allowUnverifiedWebhooks`, which would then also silence a genuinely missing
383
+ * next-gen secret.
384
+ */
385
+ get webhookVerification() {
386
+ return this.#webhookSecret !== undefined ? 'configured' : 'unsupported';
387
+ }
388
+ async parseWebhook(rawBody, headers) {
389
+ if (this.#webhookSecret !== undefined) {
390
+ this.#verifySignature(rawBody, headerValue(headers, 'x-mollie-signature'));
391
+ }
392
+ const envelope = this.#parseEventPayload(rawBody);
393
+ const entityId = envelope !== null ? envelope.entityId : this.#classicId(rawBody);
394
+ if (entityId === undefined || entityId === '') {
395
+ throw new Error('[payments] Mollie webhook body carried no resource id.');
396
+ }
397
+ if (entityId.startsWith('chb_')) {
398
+ return this.#chargebackEvent(envelope, entityId);
399
+ }
400
+ if (!entityId.startsWith('tr_')) {
401
+ // A next-gen event about something that is not a payment (a payment link, a sales
402
+ // invoice). There is nothing to map onto the payment shape, so report what the
403
+ // signed envelope said and leave it to an app handler.
404
+ return {
405
+ id: envelope?.id ?? `mollie:${entityId}`,
406
+ provider: this.provider,
407
+ type: this.#mapEventType(envelope?.type),
408
+ ...(envelope?.createdAt !== undefined ? { createdAt: envelope.createdAt } : {}),
409
+ data: { gatewayId: entityId },
410
+ raw: (envelope ?? { id: entityId }),
411
+ };
412
+ }
413
+ const data = await this.#request(`/payments/${entityId}`);
414
+ const payment = this.#mapPayment(data);
415
+ return {
416
+ // Stable per *transition*, so a redelivery of the same status dedupes in the ledger
417
+ // while the next status still gets through. A next-gen event has a real id of its own.
418
+ //
419
+ // Mollie's own `status` stays `paid` through a refund AND through a chargeback, so
420
+ // the derived id needs the mapped outcome too — without it the chargeback's event id
421
+ // is byte-identical to the earlier `payment.succeeded` one and the ledger discards
422
+ // the webhook that takes the money away as a replay.
423
+ id: envelope?.id ?? `mollie:${data.id}:${this.#transition(data.status, payment.status)}`,
424
+ provider: this.provider,
425
+ type: payment.status === 'disputed'
426
+ ? 'payment.disputed'
427
+ : payment.status === 'paid'
428
+ ? 'payment.succeeded'
429
+ : payment.status === 'refunded'
430
+ ? 'payment.refunded'
431
+ : payment.status === 'failed' || payment.status === 'canceled'
432
+ ? 'payment.failed'
433
+ : 'payment.updated',
434
+ createdAt: data.createdAt,
435
+ data: {
436
+ gatewayId: payment.gatewayId,
437
+ amount: payment.amount.amount,
438
+ currency: payment.amount.currency,
439
+ ...(payment.customerId !== undefined ? { customerId: payment.customerId } : {}),
440
+ ...(payment.subscriptionId !== undefined ? { subscriptionId: payment.subscriptionId } : {}),
441
+ ...(typeof data.metadata?.externalReference === 'string'
442
+ ? { externalReference: data.metadata.externalReference }
443
+ : {}),
444
+ ...(data.metadata != null ? { metadata: data.metadata } : {}),
445
+ },
446
+ // The request body carried only an id; the fetched payment is the gateway payload
447
+ // this event was actually normalized from.
448
+ raw: data,
449
+ };
450
+ }
451
+ /**
452
+ * A next-gen `chargeback.*` event: the one webhook that takes revenue away, and the one
453
+ * that gives it back.
454
+ *
455
+ * Mollie has exactly two of them — `chargeback.received` ("a chargeback has been received
456
+ * for a payment") and `chargeback.reversed` ("a previously received chargeback has been
457
+ * reversed") — and **no pre-dispute vocabulary at all**: no fraud alert, no retrieval
458
+ * request, no inquiry, and no `payment.dispute_warning` for this driver to emit. Mollie's
459
+ * own words are that the money "will be reclaimed and deducted from your Mollie balance",
460
+ * so the first event already is the withdrawal.
461
+ *
462
+ * The chargeback object carries no response deadline either — its whole field list is
463
+ * `id`, `amount`, `settlementAmount`, `reason`, `paymentId`, `settlementId`, `createdAt`,
464
+ * `reversedAt` — so no `actionableUntil` is emitted rather than one being invented.
465
+ *
466
+ * The reversal is read from `reversedAt` as well as from the event name: the payload is a
467
+ * snapshot of the entity, so a `chargeback.received` redelivered after the reversal
468
+ * carries the timestamp too, and taking the money back off the row twice would be worse
469
+ * than reading it from either place.
470
+ *
471
+ * The chargeback entity has to come from the event body. Mollie's only read endpoint is
472
+ * `GET /payments/{paymentId}/chargebacks/{id}` — there is no lookup by chargeback id —
473
+ * so a webhook configured with the "simple payload" (an `entityId` and nothing else)
474
+ * carries a chargeback that literally cannot be resolved. That throws rather than
475
+ * degrading to an inert event: the route answers 400, Mollie retries, and the message
476
+ * says which setting to change. A silent pass-through here is exactly the shape of bug
477
+ * `payment.disputed` exists to remove.
478
+ *
479
+ * Note this is the *next-gen* path only. On the classic webhook a chargeback arrives as
480
+ * an ordinary `tr_…` notification and is caught by `amountChargedBack` on the fetched
481
+ * payment — see {@link MollieDriver.#mapPayment}.
482
+ */
483
+ #chargebackEvent(envelope, entityId) {
484
+ const entity = envelope?._embedded?.entity;
485
+ if (entity?.paymentId === undefined || entity.amount === undefined) {
486
+ throw new Error(`[payments] Mollie sent chargeback ${entityId} with no embedded entity, and a chargeback cannot be read back on its own — \`GET /payments/{paymentId}/chargebacks/{id}\` needs the payment id Mollie did not send. Configure this webhook to deliver the full snapshot payload instead of the id-only one.`);
487
+ }
488
+ const amount = this.#fromMollieAmount(entity.amount);
489
+ const reversed = envelope?.type === 'chargeback.reversed' || typeof entity.reversedAt === 'string';
490
+ return {
491
+ id: envelope?.id ?? `mollie:${entityId}:${envelope?.type ?? 'chargeback'}`,
492
+ provider: this.provider,
493
+ // A reversal is the dispute resolved in your favour and the money back on the
494
+ // balance, so it closes the dispute as `won` — the processor is what moves the row
495
+ // off `disputed`, and reporting the reversal as a plain update left it stuck there
496
+ // with the revenue written off.
497
+ type: reversed ? 'payment.dispute_closed' : 'payment.disputed',
498
+ ...(envelope?.createdAt !== undefined ? { createdAt: envelope.createdAt } : {}),
499
+ // Keyed by the PAYMENT, not the chargeback: the payment is the row whose status has
500
+ // to move to `disputed`.
501
+ data: {
502
+ gatewayId: entity.paymentId,
503
+ amount: amount.amount,
504
+ currency: amount.currency,
505
+ disputeId: entity.id ?? entityId,
506
+ ...(reversed ? { outcome: 'won' } : {}),
507
+ // Only ever set on a SEPA Direct Debit chargeback — Mollie's reference says the
508
+ // card schemes send it no reason at all.
509
+ ...(entity.reason?.code !== undefined ? { reason: entity.reason.code } : {}),
510
+ },
511
+ raw: (envelope ?? { id: entityId }),
512
+ };
513
+ }
514
+ // ── Mappers ──────────────────────────────────────────────────────────────────────────
515
+ #mapCustomer(data) {
516
+ const taxId = data.metadata?.taxId;
517
+ return {
518
+ id: data.id,
519
+ ...(data.name != null ? { name: data.name } : {}),
520
+ ...(data.email != null ? { email: data.email } : {}),
521
+ ...(typeof taxId === 'string' ? { taxId } : {}),
522
+ ...(data.metadata != null ? { metadata: data.metadata } : {}),
523
+ };
524
+ }
525
+ #mapPayment(data) {
526
+ const amount = this.#fromMollieAmount(data.amount);
527
+ const refunded = data.amountRefunded !== undefined &&
528
+ this.#fromMollieAmount(data.amountRefunded).amount >= amount.amount;
529
+ // A chargeback leaves Mollie's own `status` at `paid` — `amountChargedBack` is the only
530
+ // field that says the bank pulled the money back, and it outranks everything below it.
531
+ const chargedBack = data.amountChargedBack !== undefined &&
532
+ this.#fromMollieAmount(data.amountChargedBack).amount > 0;
533
+ const statusMap = {
534
+ open: 'pending',
535
+ pending: 'pending',
536
+ // Funds held, nothing captured — a real Mollie state for pay-later methods and for
537
+ // manual-capture cards. It used to collapse into `pending`, which understated it.
538
+ authorized: 'authorized',
539
+ paid: 'paid',
540
+ canceled: 'canceled',
541
+ // The payer never completed it and now cannot — the same outcome as `failed`, and
542
+ // `canceled` would wrongly suggest somebody canceled it.
543
+ expired: 'failed',
544
+ failed: 'failed',
545
+ };
546
+ const result = {
547
+ id: data.id,
548
+ gatewayId: data.id,
549
+ provider: this.provider,
550
+ amount,
551
+ status: chargedBack
552
+ ? 'disputed'
553
+ : refunded
554
+ ? 'refunded'
555
+ : (statusMap[data.status] ?? 'pending'),
556
+ payload: data,
557
+ createdAt: data.createdAt,
558
+ };
559
+ if (data.customerId !== undefined)
560
+ result.customerId = data.customerId;
561
+ if (data.subscriptionId !== undefined)
562
+ result.subscriptionId = data.subscriptionId;
563
+ if (data.paidAt !== undefined)
564
+ result.paidAt = data.paidAt;
565
+ const method = this.#mapPaymentMethodType(data.method);
566
+ if (method !== undefined)
567
+ result.method = method;
568
+ const checkout = data._links?.checkout?.href;
569
+ if (checkout !== undefined)
570
+ result.hostedUrl = checkout;
571
+ return result;
572
+ }
573
+ #mapSubscription(data) {
574
+ this.#subscriptionCustomers.set(data.id, data.customerId);
575
+ const statusMap = {
576
+ pending: 'incomplete',
577
+ active: 'active',
578
+ canceled: 'canceled',
579
+ suspended: 'past_due',
580
+ completed: 'ended',
581
+ };
582
+ const planId = data.metadata?.planId;
583
+ return {
584
+ id: data.id,
585
+ gatewayId: data.id,
586
+ provider: this.provider,
587
+ customerId: data.customerId,
588
+ status: statusMap[data.status] ?? 'active',
589
+ planId: typeof planId === 'string' ? planId : (data.description ?? data.interval),
590
+ amount: this.#fromMollieAmount(data.amount),
591
+ ...(data.canceledAt != null ? { endsAt: data.canceledAt } : {}),
592
+ ...(data.nextPaymentDate != null ? { currentPeriodEnd: data.nextPaymentDate } : {}),
593
+ payload: data,
594
+ createdAt: data.createdAt,
595
+ };
596
+ }
597
+ /**
598
+ * Canonical method name → what goes in Mollie's `method` field. Anything else is a
599
+ * routing mistake and fails here rather than at the gateway.
600
+ */
601
+ #mapMethod(method, metadata) {
602
+ const brand = metadata?.mollieMethod;
603
+ const category = method === undefined || method === 'undefined' ? undefined : method;
604
+ if (typeof brand === 'string') {
605
+ if (category !== undefined) {
606
+ const allowed = MOLLIE_METHOD_CATEGORIES[category];
607
+ if (allowed === undefined)
608
+ throw this.#unknownMethod(category);
609
+ if (!allowed.includes(brand)) {
610
+ throw new Error(`[payments] Mollie method "${brand}" is not a \`${category}\` method. \`metadata.mollieMethod\` names the exact Mollie id and must belong to the category being routed — ${category} covers: ${allowed.join(', ')}.`);
611
+ }
612
+ }
613
+ return brand;
614
+ }
615
+ if (category === undefined)
616
+ return undefined;
617
+ const ids = MOLLIE_METHOD_CATEGORIES[category];
618
+ if (ids === undefined)
619
+ throw this.#unknownMethod(category);
620
+ // A single-member category goes out as the bare id Mollie documents, not a 1-element
621
+ // array, so the request body reads the way Mollie's own examples do.
622
+ return ids.length === 1 ? ids[0] : [...ids];
623
+ }
624
+ #unknownMethod(method) {
625
+ return new Error(`[payments] Mollie has no "${method}" method. This driver routes the categories ${Object.keys(MOLLIE_METHOD_CATEGORIES).join(', ')}, or lets the shopper pick on the hosted page when no method is given. Mollie has no separate debit-card method (debit cards are \`creditcard\`), and no Pix or boleto.`);
626
+ }
627
+ /**
628
+ * A Mollie method id, as reported back on a payment, onto the contract's categories.
629
+ * Only `creditcard` had a name before, so an iDEAL or Klarna payment came back with no
630
+ * `method` at all.
631
+ */
632
+ #mapPaymentMethodType(method) {
633
+ if (method === null || method === undefined)
634
+ return undefined;
635
+ if (method === 'creditcard' || method === 'pointofsale')
636
+ return 'card';
637
+ for (const [category, ids] of Object.entries(MOLLIE_METHOD_CATEGORIES)) {
638
+ if (!ids.includes(method))
639
+ continue;
640
+ if (category === 'credit_card')
641
+ return 'card';
642
+ return category;
643
+ }
644
+ // A method Mollie added since this table was written: it was paid, and saying so with
645
+ // `unknown` is honest, where leaving it unset would read as "Mollie did not tell us".
646
+ return 'unknown';
647
+ }
648
+ #mapCycle(cycle) {
649
+ switch (cycle) {
650
+ case 'WEEKLY':
651
+ return '1 week';
652
+ case 'BIWEEKLY':
653
+ return '2 weeks';
654
+ case 'QUARTERLY':
655
+ return '3 months';
656
+ case 'SEMIANNUALLY':
657
+ return '6 months';
658
+ case 'YEARLY':
659
+ return '12 months';
660
+ default:
661
+ return '1 month';
662
+ }
663
+ }
664
+ #mapEventType(type) {
665
+ switch (type) {
666
+ case 'payment.paid':
667
+ return 'payment.succeeded';
668
+ case 'payment.failed':
669
+ case 'payment.expired':
670
+ case 'payment.canceled':
671
+ return 'payment.failed';
672
+ case 'payment.refunded':
673
+ case 'refund.successful':
674
+ return 'payment.refunded';
675
+ case 'chargeback.received':
676
+ return 'payment.disputed';
677
+ default:
678
+ return type ?? 'payment.updated';
679
+ }
680
+ }
681
+ /**
682
+ * The event-id suffix for a payment webhook. Mollie's own `status` is the transition in
683
+ * every ordinary case, but it stays `paid` through both a refund and a chargeback — so
684
+ * those two get their own suffix, or their event id collides with the earlier
685
+ * `payment.succeeded` one and the idempotency ledger drops them as replays.
686
+ */
687
+ #transition(gatewayStatus, mapped) {
688
+ if (mapped === 'disputed')
689
+ return `${gatewayStatus}:chargeback`;
690
+ if (mapped === 'refunded')
691
+ return `${gatewayStatus}:refunded`;
692
+ return gatewayStatus;
693
+ }
694
+ // ── Helpers ──────────────────────────────────────────────────────────────────────────
695
+ /**
696
+ * Mollie takes money as a decimal string carrying the currency's own number of decimals
697
+ * — `"19.90"` for EUR, `"1990"` for JPY, `"1.990"` for KWD. `formatDecimal` shifts the
698
+ * digits of the integer instead of dividing, so nothing is lost to a binary float and a
699
+ * zero-decimal currency is not billed at a hundredth of its amount.
700
+ */
701
+ #toMollieAmount(amount, currency) {
702
+ const resolved = currency ?? this.#currency;
703
+ return { currency: resolved.toUpperCase(), value: formatDecimal(amount, resolved) };
704
+ }
705
+ #fromMollieAmount(amount) {
706
+ return {
707
+ amount: fromDecimal(Number(amount.value), amount.currency),
708
+ currency: amount.currency.toLowerCase(),
709
+ };
710
+ }
711
+ /**
712
+ * Pull the per-request URLs out of `metadata`: `ChargeInput` has no room for the redirect
713
+ * and webhook URLs Mollie needs, and echoing them back as gateway metadata would be noise.
714
+ */
715
+ #transportOptions(metadata) {
716
+ // `mollieMethod` is read as an argument (the exact Mollie method id to pin), so it is
717
+ // consumed here rather than echoed back as gateway metadata.
718
+ const { redirectUrl, cancelUrl, webhookUrl, mollieMethod: _mollieMethod, ...rest } = metadata ?? {};
719
+ return {
720
+ ...(typeof redirectUrl === 'string' ? { redirectUrl } : {}),
721
+ ...(typeof cancelUrl === 'string' ? { cancelUrl } : {}),
722
+ ...(typeof webhookUrl === 'string' ? { webhookUrl } : {}),
723
+ rest,
724
+ };
725
+ }
726
+ #metadataBody(rest, externalReference) {
727
+ const metadata = {
728
+ ...rest,
729
+ ...(externalReference !== undefined ? { externalReference } : {}),
730
+ };
731
+ return Object.keys(metadata).length > 0 ? { metadata } : {};
732
+ }
733
+ #customerMetadata(input) {
734
+ // Mollie's customer has no tax-id field, so it rides along in metadata — the one place
735
+ // it survives a round-trip, and where `#mapCustomer` reads it back from.
736
+ const metadata = {
737
+ ...(input.metadata ?? {}),
738
+ ...(input.taxId !== undefined ? { taxId: input.taxId } : {}),
739
+ };
740
+ return Object.keys(metadata).length > 0 ? { metadata } : {};
741
+ }
742
+ /**
743
+ * Mollie deduplicates on the `Idempotency-Key` request header and on nothing else — a key
744
+ * written into `metadata` would be echoed back and protect nothing. Mollie keeps a key for
745
+ * one hour; after that the same key creates a second payment.
746
+ *
747
+ * Applied to every POST the driver makes: `charge`, `createCheckout`, `refund`,
748
+ * `createCustomer` and `createSubscription`. `updateSubscription` is a PATCH, which
749
+ * Mollie does not accept keys for at all, so it refuses instead.
750
+ */
751
+ #idempotency(key) {
752
+ return key !== undefined ? { headers: { 'Idempotency-Key': key } } : {};
753
+ }
754
+ /**
755
+ * Every Mollie subscription endpoint is nested under a customer, but the driver contract
756
+ * passes only the subscription id. Resolved from the pairing this process already saw,
757
+ * or from an explicit `customerId/subscriptionId` — never guessed.
758
+ */
759
+ async #resolveSubscription(gatewayId) {
760
+ const slash = gatewayId.indexOf('/');
761
+ if (slash !== -1) {
762
+ return {
763
+ customerId: gatewayId.slice(0, slash),
764
+ subscriptionId: gatewayId.slice(slash + 1),
765
+ };
766
+ }
767
+ const known = this.#subscriptionCustomers.get(gatewayId);
768
+ if (known !== undefined)
769
+ return { customerId: known, subscriptionId: gatewayId };
770
+ // Last resort: Mollie's account-wide subscription list, which does carry `customerId`.
771
+ const page = await this.#request('/subscriptions?limit=250');
772
+ const found = page._embedded?.subscriptions?.find((item) => item.id === gatewayId);
773
+ if (found !== undefined) {
774
+ this.#subscriptionCustomers.set(found.id, found.customerId);
775
+ return { customerId: found.customerId, subscriptionId: found.id };
776
+ }
777
+ throw new Error(`[payments] Mollie subscription ${gatewayId} needs its customer: every subscription endpoint is nested under one, and it was not in the first page of the account-wide list. Pass it as \`"cst_xxx/sub_xxx"\`.`);
778
+ }
779
+ /** Next-gen webhook envelope, or `null` when this is the classic form-encoded id POST. */
780
+ #parseEventPayload(rawBody) {
781
+ if (!rawBody.trimStart().startsWith('{'))
782
+ return null;
783
+ const parsed = JSON.parse(rawBody);
784
+ return parsed.resource === 'event' ? parsed : null;
785
+ }
786
+ #verifySignature(rawBody, signature) {
787
+ if (signature === undefined || signature === '') {
788
+ throw new Error('[payments] Missing X-Mollie-Signature on Mollie webhook request.');
789
+ }
790
+ // Mollie sends `sha256=<hex>`; the digest is over the raw, undeserialized body.
791
+ const received = signature.startsWith('sha256=') ? signature.slice(7) : signature;
792
+ const secret = this.#webhookSecret ?? '';
793
+ const expected = createHmac('sha256', secret).update(rawBody, 'utf8').digest('hex');
794
+ if (!safeCompare(received, expected)) {
795
+ throw new Error('[payments] Invalid Mollie webhook signature.');
796
+ }
797
+ }
798
+ /** The classic webhook's only field: `id=tr_xxx`, form-encoded. */
799
+ #classicId(rawBody) {
800
+ return new URLSearchParams(rawBody).get('id') ?? undefined;
801
+ }
802
+ async #request(path, options = {}) {
803
+ return httpRequest(path, {
804
+ baseUrl: this.#baseUrl,
805
+ ...(options.method !== undefined ? { method: options.method } : {}),
806
+ ...(options.body !== undefined ? { body: options.body } : {}),
807
+ ...(options.headers !== undefined ? { headers: options.headers } : {}),
808
+ bearerToken: this.#apiKey,
809
+ });
810
+ }
811
+ }
812
+ //# sourceMappingURL=mollie.js.map