@adonis-agora/payments 0.2.0 → 0.4.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 (346) hide show
  1. package/CHANGELOG.md +2375 -0
  2. package/dist/assets/spa/assets/index-B7rU8eIL.css +2 -0
  3. package/dist/assets/spa/assets/index-BcM2VH5k.js +10 -0
  4. package/dist/assets/spa/assets/index-BcM2VH5k.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/make_billable.d.ts.map +1 -1
  11. package/dist/commands/make_billable.js +1 -1
  12. package/dist/commands/make_billable.js.map +1 -1
  13. package/dist/commands/make_webhook_handler.d.ts.map +1 -1
  14. package/dist/commands/make_webhook_handler.js +1 -1
  15. package/dist/commands/make_webhook_handler.js.map +1 -1
  16. package/dist/commands/payments_health.d.ts +31 -0
  17. package/dist/commands/payments_health.d.ts.map +1 -0
  18. package/dist/commands/payments_health.js +113 -0
  19. package/dist/commands/payments_health.js.map +1 -0
  20. package/dist/commands/payments_sync.d.ts +18 -4
  21. package/dist/commands/payments_sync.d.ts.map +1 -1
  22. package/dist/commands/payments_sync.js +136 -40
  23. package/dist/commands/payments_sync.js.map +1 -1
  24. package/dist/commands/payments_webhook.d.ts.map +1 -1
  25. package/dist/commands/payments_webhook.js.map +1 -1
  26. package/dist/configure.d.ts +12 -5
  27. package/dist/configure.d.ts.map +1 -1
  28. package/dist/configure.js +18 -5
  29. package/dist/configure.js.map +1 -1
  30. package/dist/providers/dashboard_provider.d.ts +97 -0
  31. package/dist/providers/dashboard_provider.d.ts.map +1 -0
  32. package/dist/providers/dashboard_provider.js +430 -0
  33. package/dist/providers/dashboard_provider.js.map +1 -0
  34. package/dist/providers/payments_client_provider.d.ts +41 -0
  35. package/dist/providers/payments_client_provider.d.ts.map +1 -0
  36. package/dist/providers/payments_client_provider.js +95 -0
  37. package/dist/providers/payments_client_provider.js.map +1 -0
  38. package/dist/providers/payments_provider.d.ts +35 -0
  39. package/dist/providers/payments_provider.d.ts.map +1 -1
  40. package/dist/providers/payments_provider.js +250 -36
  41. package/dist/providers/payments_provider.js.map +1 -1
  42. package/dist/src/billing/billing_health.d.ts +85 -0
  43. package/dist/src/billing/billing_health.d.ts.map +1 -0
  44. package/dist/src/billing/billing_health.js +155 -0
  45. package/dist/src/billing/billing_health.js.map +1 -0
  46. package/dist/src/billing/billing_overview.d.ts +4 -3
  47. package/dist/src/billing/billing_overview.d.ts.map +1 -1
  48. package/dist/src/billing/billing_overview.js +14 -5
  49. package/dist/src/billing/billing_overview.js.map +1 -1
  50. package/dist/src/billing/billing_store.d.ts +634 -5
  51. package/dist/src/billing/billing_store.d.ts.map +1 -1
  52. package/dist/src/billing/billing_store.js +18 -1
  53. package/dist/src/billing/billing_store.js.map +1 -1
  54. package/dist/src/billing/index.d.ts +4 -3
  55. package/dist/src/billing/index.d.ts.map +1 -1
  56. package/dist/src/billing/index.js +2 -1
  57. package/dist/src/billing/index.js.map +1 -1
  58. package/dist/src/billing/list_query.d.ts +16 -0
  59. package/dist/src/billing/list_query.d.ts.map +1 -0
  60. package/dist/src/billing/list_query.js +28 -0
  61. package/dist/src/billing/list_query.js.map +1 -0
  62. package/dist/src/billing/lucid_billing_store.d.ts +122 -4
  63. package/dist/src/billing/lucid_billing_store.d.ts.map +1 -1
  64. package/dist/src/billing/lucid_billing_store.js +811 -13
  65. package/dist/src/billing/lucid_billing_store.js.map +1 -1
  66. package/dist/src/billing/mixins/index.d.ts +5 -2
  67. package/dist/src/billing/mixins/index.d.ts.map +1 -1
  68. package/dist/src/billing/mixins/index.js +5 -2
  69. package/dist/src/billing/mixins/index.js.map +1 -1
  70. package/dist/src/billing/mixins/with_audit_event.d.ts +48 -0
  71. package/dist/src/billing/mixins/with_audit_event.d.ts.map +1 -0
  72. package/dist/src/billing/mixins/with_audit_event.js +77 -0
  73. package/dist/src/billing/mixins/with_audit_event.js.map +1 -0
  74. package/dist/src/billing/mixins/with_billable.d.ts.map +1 -1
  75. package/dist/src/billing/mixins/with_customer.d.ts +39 -0
  76. package/dist/src/billing/mixins/with_customer.d.ts.map +1 -0
  77. package/dist/src/billing/mixins/with_customer.js +68 -0
  78. package/dist/src/billing/mixins/with_customer.js.map +1 -0
  79. package/dist/src/billing/mixins/with_dispute.d.ts +68 -0
  80. package/dist/src/billing/mixins/with_dispute.d.ts.map +1 -0
  81. package/dist/src/billing/mixins/with_dispute.js +83 -0
  82. package/dist/src/billing/mixins/with_dispute.js.map +1 -0
  83. package/dist/src/billing/mixins/with_payment.d.ts +42 -0
  84. package/dist/src/billing/mixins/with_payment.d.ts.map +1 -1
  85. package/dist/src/billing/mixins/with_payment.js +29 -3
  86. package/dist/src/billing/mixins/with_payment.js.map +1 -1
  87. package/dist/src/billing/mixins/with_subscription.d.ts +7 -0
  88. package/dist/src/billing/mixins/with_subscription.d.ts.map +1 -1
  89. package/dist/src/billing/mixins/with_subscription.js +15 -1
  90. package/dist/src/billing/mixins/with_subscription.js.map +1 -1
  91. package/dist/src/billing/mixins/with_usage_event.d.ts +7 -0
  92. package/dist/src/billing/mixins/with_usage_event.d.ts.map +1 -1
  93. package/dist/src/billing/mixins/with_usage_event.js +15 -1
  94. package/dist/src/billing/mixins/with_usage_event.js.map +1 -1
  95. package/dist/src/billing/mixins/with_webhook_event.d.ts +18 -0
  96. package/dist/src/billing/mixins/with_webhook_event.d.ts.map +1 -1
  97. package/dist/src/billing/mixins/with_webhook_event.js +18 -1
  98. package/dist/src/billing/mixins/with_webhook_event.js.map +1 -1
  99. package/dist/src/billing/resolve_dispatch.d.ts +22 -0
  100. package/dist/src/billing/resolve_dispatch.d.ts.map +1 -0
  101. package/dist/src/billing/resolve_dispatch.js +44 -0
  102. package/dist/src/billing/resolve_dispatch.js.map +1 -0
  103. package/dist/src/billing/resolve_store.d.ts +18 -0
  104. package/dist/src/billing/resolve_store.d.ts.map +1 -0
  105. package/dist/src/billing/resolve_store.js +35 -0
  106. package/dist/src/billing/resolve_store.js.map +1 -0
  107. package/dist/src/billing/schema.d.ts +108 -0
  108. package/dist/src/billing/schema.d.ts.map +1 -0
  109. package/dist/src/billing/schema.js +350 -0
  110. package/dist/src/billing/schema.js.map +1 -0
  111. package/dist/src/billing/webhook_dispatcher.d.ts +95 -16
  112. package/dist/src/billing/webhook_dispatcher.d.ts.map +1 -1
  113. package/dist/src/billing/webhook_dispatcher.js +186 -33
  114. package/dist/src/billing/webhook_dispatcher.js.map +1 -1
  115. package/dist/src/billing/webhook_events.d.ts +67 -1
  116. package/dist/src/billing/webhook_events.d.ts.map +1 -1
  117. package/dist/src/billing/webhook_events.js +15 -0
  118. package/dist/src/billing/webhook_events.js.map +1 -1
  119. package/dist/src/billing/webhook_processor.d.ts.map +1 -1
  120. package/dist/src/billing/webhook_processor.js +325 -5
  121. package/dist/src/billing/webhook_processor.js.map +1 -1
  122. package/dist/src/client/define_config.d.ts +150 -0
  123. package/dist/src/client/define_config.d.ts.map +1 -0
  124. package/dist/src/client/define_config.js +98 -0
  125. package/dist/src/client/define_config.js.map +1 -0
  126. package/dist/src/client/handlers.d.ts +71 -0
  127. package/dist/src/client/handlers.d.ts.map +1 -0
  128. package/dist/src/client/handlers.js +119 -0
  129. package/dist/src/client/handlers.js.map +1 -0
  130. package/dist/src/client/index.d.ts +15 -0
  131. package/dist/src/client/index.d.ts.map +1 -0
  132. package/dist/src/client/index.js +12 -0
  133. package/dist/src/client/index.js.map +1 -0
  134. package/dist/src/client/statuses.d.ts +38 -0
  135. package/dist/src/client/statuses.d.ts.map +1 -0
  136. package/dist/src/client/statuses.js +42 -0
  137. package/dist/src/client/statuses.js.map +1 -0
  138. package/dist/src/dashboard/actions.d.ts +143 -0
  139. package/dist/src/dashboard/actions.d.ts.map +1 -0
  140. package/dist/src/dashboard/actions.js +112 -0
  141. package/dist/src/dashboard/actions.js.map +1 -0
  142. package/dist/src/dashboard/auth.d.ts +136 -0
  143. package/dist/src/dashboard/auth.d.ts.map +1 -0
  144. package/dist/src/dashboard/auth.js +152 -0
  145. package/dist/src/dashboard/auth.js.map +1 -0
  146. package/dist/src/dashboard/define_config.d.ts +80 -0
  147. package/dist/src/dashboard/define_config.d.ts.map +1 -0
  148. package/dist/src/dashboard/define_config.js +83 -0
  149. package/dist/src/dashboard/define_config.js.map +1 -0
  150. package/dist/src/dashboard/handlers.d.ts +281 -0
  151. package/dist/src/dashboard/handlers.d.ts.map +1 -0
  152. package/dist/src/dashboard/handlers.js +960 -0
  153. package/dist/src/dashboard/handlers.js.map +1 -0
  154. package/dist/src/dashboard/index.d.ts +17 -0
  155. package/dist/src/dashboard/index.d.ts.map +1 -0
  156. package/dist/src/dashboard/index.js +13 -0
  157. package/dist/src/dashboard/index.js.map +1 -0
  158. package/dist/src/dashboard/login_page.d.ts +23 -0
  159. package/dist/src/dashboard/login_page.d.ts.map +1 -0
  160. package/dist/src/dashboard/login_page.js +170 -0
  161. package/dist/src/dashboard/login_page.js.map +1 -0
  162. package/dist/src/dashboard/period.d.ts +31 -0
  163. package/dist/src/dashboard/period.d.ts.map +1 -0
  164. package/dist/src/dashboard/period.js +44 -0
  165. package/dist/src/dashboard/period.js.map +1 -0
  166. package/dist/src/dashboard/session_cookie.d.ts +42 -0
  167. package/dist/src/dashboard/session_cookie.d.ts.map +1 -0
  168. package/dist/src/dashboard/session_cookie.js +97 -0
  169. package/dist/src/dashboard/session_cookie.js.map +1 -0
  170. package/dist/src/dashboard/spa.d.ts +24 -0
  171. package/dist/src/dashboard/spa.d.ts.map +1 -0
  172. package/dist/src/dashboard/spa.js +52 -0
  173. package/dist/src/dashboard/spa.js.map +1 -0
  174. package/dist/src/define_config.d.ts +245 -10
  175. package/dist/src/define_config.d.ts.map +1 -1
  176. package/dist/src/define_config.js +97 -1
  177. package/dist/src/define_config.js.map +1 -1
  178. package/dist/src/diagnostics.d.ts +287 -7
  179. package/dist/src/diagnostics.d.ts.map +1 -1
  180. package/dist/src/diagnostics.js +317 -1
  181. package/dist/src/diagnostics.js.map +1 -1
  182. package/dist/src/driver.d.ts +139 -5
  183. package/dist/src/driver.d.ts.map +1 -1
  184. package/dist/src/drivers/abacate.d.ts +13 -3
  185. package/dist/src/drivers/abacate.d.ts.map +1 -1
  186. package/dist/src/drivers/abacate.js +112 -19
  187. package/dist/src/drivers/abacate.js.map +1 -1
  188. package/dist/src/drivers/adyen.d.ts +164 -0
  189. package/dist/src/drivers/adyen.d.ts.map +1 -0
  190. package/dist/src/drivers/adyen.js +678 -0
  191. package/dist/src/drivers/adyen.js.map +1 -0
  192. package/dist/src/drivers/asaas.d.ts +53 -3
  193. package/dist/src/drivers/asaas.d.ts.map +1 -1
  194. package/dist/src/drivers/asaas.js +364 -29
  195. package/dist/src/drivers/asaas.js.map +1 -1
  196. package/dist/src/drivers/dodo.d.ts +135 -0
  197. package/dist/src/drivers/dodo.d.ts.map +1 -0
  198. package/dist/src/drivers/dodo.js +864 -0
  199. package/dist/src/drivers/dodo.js.map +1 -0
  200. package/dist/src/drivers/efi.d.ts +98 -0
  201. package/dist/src/drivers/efi.d.ts.map +1 -0
  202. package/dist/src/drivers/efi.js +628 -0
  203. package/dist/src/drivers/efi.js.map +1 -0
  204. package/dist/src/drivers/infinitepay.d.ts +156 -0
  205. package/dist/src/drivers/infinitepay.d.ts.map +1 -0
  206. package/dist/src/drivers/infinitepay.js +321 -0
  207. package/dist/src/drivers/infinitepay.js.map +1 -0
  208. package/dist/src/drivers/lemonsqueezy.d.ts +125 -0
  209. package/dist/src/drivers/lemonsqueezy.d.ts.map +1 -0
  210. package/dist/src/drivers/lemonsqueezy.js +632 -0
  211. package/dist/src/drivers/lemonsqueezy.js.map +1 -0
  212. package/dist/src/drivers/mercadopago.d.ts +97 -0
  213. package/dist/src/drivers/mercadopago.d.ts.map +1 -0
  214. package/dist/src/drivers/mercadopago.js +844 -0
  215. package/dist/src/drivers/mercadopago.js.map +1 -0
  216. package/dist/src/drivers/mollie.d.ts +125 -0
  217. package/dist/src/drivers/mollie.d.ts.map +1 -0
  218. package/dist/src/drivers/mollie.js +812 -0
  219. package/dist/src/drivers/mollie.js.map +1 -0
  220. package/dist/src/drivers/paddle.d.ts +148 -0
  221. package/dist/src/drivers/paddle.d.ts.map +1 -0
  222. package/dist/src/drivers/paddle.js +738 -0
  223. package/dist/src/drivers/paddle.js.map +1 -0
  224. package/dist/src/drivers/pagarme.d.ts +97 -0
  225. package/dist/src/drivers/pagarme.d.ts.map +1 -0
  226. package/dist/src/drivers/pagarme.js +829 -0
  227. package/dist/src/drivers/pagarme.js.map +1 -0
  228. package/dist/src/drivers/pagbank.d.ts +57 -0
  229. package/dist/src/drivers/pagbank.d.ts.map +1 -0
  230. package/dist/src/drivers/pagbank.js +499 -0
  231. package/dist/src/drivers/pagbank.js.map +1 -0
  232. package/dist/src/drivers/paypal.d.ts +120 -0
  233. package/dist/src/drivers/paypal.d.ts.map +1 -0
  234. package/dist/src/drivers/paypal.js +891 -0
  235. package/dist/src/drivers/paypal.js.map +1 -0
  236. package/dist/src/drivers/polar.d.ts +118 -0
  237. package/dist/src/drivers/polar.d.ts.map +1 -0
  238. package/dist/src/drivers/polar.js +604 -0
  239. package/dist/src/drivers/polar.js.map +1 -0
  240. package/dist/src/drivers/razorpay.d.ts +103 -0
  241. package/dist/src/drivers/razorpay.d.ts.map +1 -0
  242. package/dist/src/drivers/razorpay.js +867 -0
  243. package/dist/src/drivers/razorpay.js.map +1 -0
  244. package/dist/src/drivers/shared.d.ts +33 -0
  245. package/dist/src/drivers/shared.d.ts.map +1 -0
  246. package/dist/src/drivers/shared.js +37 -0
  247. package/dist/src/drivers/shared.js.map +1 -0
  248. package/dist/src/drivers/square.d.ts +133 -0
  249. package/dist/src/drivers/square.d.ts.map +1 -0
  250. package/dist/src/drivers/square.js +905 -0
  251. package/dist/src/drivers/square.js.map +1 -0
  252. package/dist/src/drivers/stripe.d.ts +41 -4
  253. package/dist/src/drivers/stripe.d.ts.map +1 -1
  254. package/dist/src/drivers/stripe.js +823 -26
  255. package/dist/src/drivers/stripe.js.map +1 -1
  256. package/dist/src/drivers/woovi.d.ts +11 -4
  257. package/dist/src/drivers/woovi.d.ts.map +1 -1
  258. package/dist/src/drivers/woovi.js +162 -29
  259. package/dist/src/drivers/woovi.js.map +1 -1
  260. package/dist/src/ensure_customer.d.ts +34 -1
  261. package/dist/src/ensure_customer.d.ts.map +1 -1
  262. package/dist/src/ensure_customer.js +29 -4
  263. package/dist/src/ensure_customer.js.map +1 -1
  264. package/dist/src/hooks/webhook_handlers.d.ts.map +1 -1
  265. package/dist/src/hooks/webhook_handlers.js.map +1 -1
  266. package/dist/src/http.d.ts +37 -3
  267. package/dist/src/http.d.ts.map +1 -1
  268. package/dist/src/http.js +69 -5
  269. package/dist/src/http.js.map +1 -1
  270. package/dist/src/index.d.ts +54 -26
  271. package/dist/src/index.d.ts.map +1 -1
  272. package/dist/src/index.js +29 -12
  273. package/dist/src/index.js.map +1 -1
  274. package/dist/src/invoice/drivers/asaas.d.ts.map +1 -1
  275. package/dist/src/invoice/drivers/asaas.js.map +1 -1
  276. package/dist/src/invoice/drivers/enotas.d.ts.map +1 -1
  277. package/dist/src/invoice/drivers/enotas.js.map +1 -1
  278. package/dist/src/invoice/drivers/focus.d.ts.map +1 -1
  279. package/dist/src/invoice/drivers/focus.js.map +1 -1
  280. package/dist/src/invoice/drivers/plugnotas.d.ts.map +1 -1
  281. package/dist/src/invoice/drivers/plugnotas.js.map +1 -1
  282. package/dist/src/invoice/drivers/tecnospeed.d.ts.map +1 -1
  283. package/dist/src/invoice/drivers/tecnospeed.js.map +1 -1
  284. package/dist/src/invoice/emit_invoice.d.ts.map +1 -1
  285. package/dist/src/invoice/emit_invoice.js +31 -11
  286. package/dist/src/invoice/emit_invoice.js.map +1 -1
  287. package/dist/src/invoice/invoice_manager.d.ts.map +1 -1
  288. package/dist/src/money.d.ts +23 -5
  289. package/dist/src/money.d.ts.map +1 -1
  290. package/dist/src/money.js +72 -7
  291. package/dist/src/money.js.map +1 -1
  292. package/dist/src/payments_manager.d.ts +8 -1
  293. package/dist/src/payments_manager.d.ts.map +1 -1
  294. package/dist/src/payments_manager.js +168 -6
  295. package/dist/src/payments_manager.js.map +1 -1
  296. package/dist/src/services/main.d.ts +38 -0
  297. package/dist/src/services/main.d.ts.map +1 -1
  298. package/dist/src/services/main.js +130 -0
  299. package/dist/src/services/main.js.map +1 -1
  300. package/dist/src/telescope/index.d.ts +2 -0
  301. package/dist/src/telescope/index.d.ts.map +1 -1
  302. package/dist/src/telescope/index.js +1 -0
  303. package/dist/src/telescope/index.js.map +1 -1
  304. package/dist/src/telescope/payments_watcher.d.ts +10 -4
  305. package/dist/src/telescope/payments_watcher.d.ts.map +1 -1
  306. package/dist/src/telescope/payments_watcher.js +66 -14
  307. package/dist/src/telescope/payments_watcher.js.map +1 -1
  308. package/dist/src/telescope/register.d.ts +62 -0
  309. package/dist/src/telescope/register.d.ts.map +1 -0
  310. package/dist/src/telescope/register.js +48 -0
  311. package/dist/src/telescope/register.js.map +1 -0
  312. package/dist/src/testing/fake_payments.d.ts +62 -0
  313. package/dist/src/testing/fake_payments.d.ts.map +1 -0
  314. package/dist/src/testing/fake_payments.js +82 -0
  315. package/dist/src/testing/fake_payments.js.map +1 -0
  316. package/dist/src/testing/fake_payments_driver.d.ts +1 -1
  317. package/dist/src/testing/fake_payments_driver.d.ts.map +1 -1
  318. package/dist/src/testing/fake_payments_driver.js.map +1 -1
  319. package/dist/src/testing/in_memory_billing_store.d.ts +151 -2
  320. package/dist/src/testing/in_memory_billing_store.d.ts.map +1 -1
  321. package/dist/src/testing/in_memory_billing_store.js +517 -4
  322. package/dist/src/testing/in_memory_billing_store.js.map +1 -1
  323. package/dist/src/testing/index.d.ts +3 -2
  324. package/dist/src/testing/index.d.ts.map +1 -1
  325. package/dist/src/testing/index.js +1 -0
  326. package/dist/src/testing/index.js.map +1 -1
  327. package/dist/src/testing/mutable_clock.d.ts.map +1 -1
  328. package/dist/src/types.d.ts +161 -5
  329. package/dist/src/types.d.ts.map +1 -1
  330. package/dist/src/types.js +30 -1
  331. package/dist/src/types.js.map +1 -1
  332. package/dist/src/webhook_handlers.d.ts +96 -2
  333. package/dist/src/webhook_handlers.d.ts.map +1 -1
  334. package/dist/src/webhook_handlers.js +91 -10
  335. package/dist/src/webhook_handlers.js.map +1 -1
  336. package/dist/src/webhook_security.d.ts +94 -3
  337. package/dist/src/webhook_security.d.ts.map +1 -1
  338. package/dist/src/webhook_security.js +173 -9
  339. package/dist/src/webhook_security.js.map +1 -1
  340. package/dist/stubs/config/payments.stub +13 -0
  341. package/dist/stubs/config/payments_client.stub +42 -0
  342. package/dist/stubs/config/payments_dashboard.stub +38 -0
  343. package/dist/stubs/database/migrations/create_billing_tables.stub +35 -77
  344. package/dist/stubs/main.js.map +1 -1
  345. package/dist/stubs/make/webhook_handler/main.stub +29 -11
  346. package/package.json +67 -32
@@ -0,0 +1,678 @@
1
+ import { createHmac } from 'node:crypto';
2
+ import { publishPaymentDiagnostics, publishRefundDiagnostics } from '../diagnostics.js';
3
+ import { httpRequest } from '../http.js';
4
+ import { emitInvoiceIfRequested } from '../invoice/emit_invoice.js';
5
+ import { safeCompare } from '../webhook_security.js';
6
+ import { requireCredential, requireCurrency } from './shared.js';
7
+ /** Adyen documents `Idempotency-Key` as at most this many characters, and recommends a UUID. */
8
+ const IDEMPOTENCY_MAX_LENGTH = 64;
9
+ /**
10
+ * Adyen driver — Checkout API v71, `X-API-Key` auth, merchant-account scoped.
11
+ *
12
+ * Adyen is a processor, not a billing system, and this driver refuses rather than
13
+ * pretends where the two disagree: there is no customer resource (a `shopperReference` is
14
+ * a string you invent), no endpoint that reads a payment back by its `pspReference`, and
15
+ * no subscription object at all — recurring billing at Adyen is you charging a stored
16
+ * token on your own schedule. Money is already an integer in minor units
17
+ * (`{ "value": 1990, "currency": "EUR" }`), so nothing here converts it.
18
+ */
19
+ export class AdyenDriver {
20
+ provider = 'adyen';
21
+ /**
22
+ * `charge()` sends a stored card token (`paymentMethod.type: 'scheme'`), and
23
+ * `createCheckout()` opens a Pay by Link page where the shopper picks. Adyen's other
24
+ * ~100 local methods only reach a shopper through Drop-in/Components, which this driver
25
+ * does not front, and the `PaymentMethodName` union has no name for them anyway.
26
+ */
27
+ supportedMethods = ['credit_card', 'undefined'];
28
+ /**
29
+ * No invoice resource in the Checkout API, and no subscription resource anywhere.
30
+ *
31
+ * `disputes: false` is deliberate and is NOT "Adyen has no dispute API" — it has one,
32
+ * Defend Disputes v30, and this driver cannot honestly drive it through the shared
33
+ * contract: v30 has no endpoint that reads a dispute back, and a defense is a
34
+ * scheme-specific `defenseReasonCode` plus base64 documents, none of which
35
+ * `DisputeEvidence` can carry. `findDispute` and `submitDisputeEvidence` below say so
36
+ * in full rather than leaving a caller with a missing method.
37
+ */
38
+ capabilities = {
39
+ disputes: false,
40
+ refunds: true,
41
+ invoices: false,
42
+ subscriptions: false,
43
+ };
44
+ #baseUrl;
45
+ #apiKey;
46
+ #merchantAccount;
47
+ #currency;
48
+ #hmacKey;
49
+ #captureMode;
50
+ #invoiceCtx;
51
+ constructor(ctx, config) {
52
+ this.#invoiceCtx = ctx;
53
+ this.#apiKey = requireCredential({
54
+ driver: 'adyen',
55
+ option: 'apiKey',
56
+ env: 'ADYEN_API_KEY',
57
+ value: config.apiKey,
58
+ });
59
+ this.#merchantAccount = requireCredential({
60
+ driver: 'adyen',
61
+ option: 'merchantAccount',
62
+ env: 'ADYEN_MERCHANT_ACCOUNT',
63
+ value: config.merchantAccount,
64
+ });
65
+ this.#currency = requireCurrency('adyen', config.currency);
66
+ this.#hmacKey = config.hmacKey ?? process.env.ADYEN_HMAC_KEY;
67
+ this.#captureMode = config.captureMode ?? 'automatic';
68
+ const environment = config.environment ?? (process.env.NODE_ENV === 'production' ? 'live' : 'test');
69
+ if (environment === 'live') {
70
+ // Live traffic goes to a per-customer host; the generic live domain does not exist,
71
+ // so this has to fail at boot rather than as a DNS error on the first charge.
72
+ const prefix = requireCredential({
73
+ driver: 'adyen',
74
+ option: 'liveUrlPrefix',
75
+ env: 'ADYEN_LIVE_URL_PREFIX',
76
+ value: config.liveUrlPrefix,
77
+ });
78
+ this.#baseUrl = `https://${prefix}-checkout-live.adyenpayments.com/checkout/v71`;
79
+ }
80
+ else {
81
+ this.#baseUrl = 'https://checkout-test.adyen.com/v71';
82
+ }
83
+ }
84
+ // ── Customers ────────────────────────────────────────────────────────────────────────
85
+ async createCustomer(_input) {
86
+ throw new Error('[payments] Adyen has no customer resource to create. Its `shopperReference` is a ' +
87
+ 'string you choose (your own user id) and it exists only on the payments that carry ' +
88
+ 'it — pass yours as `customerId` on `charge`/`createCheckout`.');
89
+ }
90
+ async findCustomer(_customerId) {
91
+ throw new Error('[payments] Adyen has no customer resource to look up. Use ' +
92
+ '`GET /v71/storedPaymentMethods?shopperReference=…` if you need the tokens saved ' +
93
+ 'for a shopper.');
94
+ }
95
+ async updateCustomer(_customerId, _input) {
96
+ throw new Error('[payments] Adyen has no customer resource to update — `shopperReference` is a plain ' +
97
+ 'string on each payment, and shopper details live in your own database.');
98
+ }
99
+ // ── Payments ─────────────────────────────────────────────────────────────────────────
100
+ async charge(input) {
101
+ if (input.split !== undefined && input.split.length > 0) {
102
+ throw new Error('[payments] Adyen splits with absolute amounts against balance accounts ' +
103
+ '(`splits[].account`), which the percent-based `split` input cannot express. ' +
104
+ 'Build the `splits` array yourself against the Adyen Checkout API.');
105
+ }
106
+ // Adyen's Checkout API has no server-side way to charge without a payment method: a
107
+ // fresh card arrives through Drop-in/Components, and a saved one is a token.
108
+ const token = input.card?.token ?? input.paymentMethodId;
109
+ if (token === undefined) {
110
+ throw new Error('[payments] Adyen needs a payment method on every charge. Pass a stored token as ' +
111
+ '`paymentMethodId` (or `card.token`), or send the shopper to ' +
112
+ '`createCheckout()` (Pay by Link).');
113
+ }
114
+ // `reference` is mandatory at Adyen and comes back as `merchantReference` on every
115
+ // webhook — the only thing tying a notification to a local row.
116
+ if (input.externalReference === undefined) {
117
+ throw new Error('[payments] Adyen requires `externalReference` on a charge: it becomes the ' +
118
+ 'payment `reference`, which the webhook echoes as `merchantReference`.');
119
+ }
120
+ const extra = this.#passthrough(input.metadata);
121
+ const data = await this.#request('/payments', {
122
+ method: 'POST',
123
+ body: {
124
+ merchantAccount: this.#merchantAccount,
125
+ amount: this.#amount(input.amount, input.currency),
126
+ reference: input.externalReference,
127
+ paymentMethod: {
128
+ type: extra.paymentMethodType ?? 'scheme',
129
+ storedPaymentMethodId: token,
130
+ },
131
+ ...(input.customerId !== undefined ? { shopperReference: input.customerId } : {}),
132
+ ...(input.customer?.email !== undefined ? { shopperEmail: input.customer.email } : {}),
133
+ // Adyen decides scheme/mandate rules from these two, and the right values are a
134
+ // business decision (is the shopper present? is this a subscription?), so the
135
+ // driver forwards yours and never invents one.
136
+ ...(extra.shopperInteraction !== undefined
137
+ ? { shopperInteraction: extra.shopperInteraction }
138
+ : {}),
139
+ ...(extra.recurringProcessingModel !== undefined
140
+ ? { recurringProcessingModel: extra.recurringProcessingModel }
141
+ : {}),
142
+ ...(extra.returnUrl !== undefined ? { returnUrl: extra.returnUrl } : {}),
143
+ ...(Object.keys(extra.rest).length > 0 ? { metadata: extra.rest } : {}),
144
+ },
145
+ ...this.#idempotency(input.idempotencyKey),
146
+ });
147
+ const payment = this.#mapPayment(data, input.amount, input.currency, input.customerId, extra.paymentMethodType ?? 'scheme');
148
+ await emitInvoiceIfRequested(this.#invoiceCtx, input, payment, this);
149
+ publishPaymentDiagnostics(payment);
150
+ return payment;
151
+ }
152
+ async findPayment(_gatewayId) {
153
+ throw new Error('[payments] Adyen Checkout v71 has no endpoint that reads a payment back by its ' +
154
+ 'pspReference — the payment state reaches you through webhooks (AUTHORISATION, ' +
155
+ 'CAPTURE, REFUND). Read your own `billing_payments` row, kept in sync by them.');
156
+ }
157
+ async refund(paymentGatewayId, amount, options) {
158
+ if (amount === undefined) {
159
+ // Adyen requires the amount and gives the driver no way to read the payment's own,
160
+ // so "full refund" cannot be inferred here without inventing a number.
161
+ throw new Error('[payments] Adyen requires the refund amount, and its Checkout API cannot read the ' +
162
+ 'payment back to infer a full refund. Pass the amount explicitly.');
163
+ }
164
+ const data = await this.#request(`/payments/${paymentGatewayId}/refunds`, {
165
+ method: 'POST',
166
+ body: {
167
+ merchantAccount: this.#merchantAccount,
168
+ amount: this.#amount(amount, undefined),
169
+ },
170
+ // The refunds endpoint takes the same `Idempotency-Key` header the payments one does,
171
+ // and it is the only thing standing between a retried job and a second refund.
172
+ ...this.#idempotency(options?.idempotencyKey),
173
+ });
174
+ const status = data.status.toLowerCase();
175
+ const refund = {
176
+ id: data.pspReference,
177
+ gatewayId: data.pspReference,
178
+ provider: this.provider,
179
+ amount: { amount: data.amount.value, currency: data.amount.currency.toLowerCase() },
180
+ // Adyen answers a refund request, not the refund itself: the outcome arrives later in
181
+ // a REFUND webhook, so anything short of `authorised` is still pending here.
182
+ status: status === 'authorised' ? 'succeeded' : status === 'refused' ? 'failed' : 'pending',
183
+ createdAt: new Date().toISOString(),
184
+ };
185
+ publishRefundDiagnostics(refund);
186
+ return refund;
187
+ }
188
+ // ── Checkout ─────────────────────────────────────────────────────────────────────────
189
+ async createCheckout(input) {
190
+ if (input.planId !== undefined || input.trialDays !== undefined) {
191
+ throw new Error('[payments] Adyen has no subscription checkout: it has no plan or subscription ' +
192
+ 'resource. Tokenize the card (`storePaymentMethod: true`) and charge it on your ' +
193
+ 'own schedule.');
194
+ }
195
+ const extra = this.#passthrough(input.metadata);
196
+ if (input.externalReference === undefined) {
197
+ throw new Error('[payments] Adyen requires `externalReference` on a payment link: it becomes the ' +
198
+ 'link `reference`, which the webhook echoes as `merchantReference`.');
199
+ }
200
+ const data = await this.#request('/paymentLinks', {
201
+ method: 'POST',
202
+ body: {
203
+ merchantAccount: this.#merchantAccount,
204
+ amount: this.#amount(input.amount, input.currency),
205
+ reference: input.externalReference,
206
+ returnUrl: input.successUrl,
207
+ ...(input.description !== undefined ? { description: input.description } : {}),
208
+ ...(input.customerId !== undefined ? { shopperReference: input.customerId } : {}),
209
+ ...(Object.keys(extra.rest).length > 0 ? { metadata: extra.rest } : {}),
210
+ },
211
+ ...this.#idempotency(input.idempotencyKey),
212
+ });
213
+ const status = data.status.toLowerCase();
214
+ return {
215
+ id: data.id,
216
+ gatewayId: data.id,
217
+ provider: this.provider,
218
+ url: data.url,
219
+ status: status === 'completed' || status === 'paid'
220
+ ? 'complete'
221
+ : status === 'expired'
222
+ ? 'expired'
223
+ : 'open',
224
+ amount: { amount: data.amount.value, currency: data.amount.currency.toLowerCase() },
225
+ ...(data.shopperReference !== undefined ? { customerId: data.shopperReference } : {}),
226
+ };
227
+ }
228
+ // ── Subscriptions ────────────────────────────────────────────────────────────────────
229
+ async createSubscription(_input) {
230
+ throw new Error('[payments] Adyen has no subscription resource: recurring billing is you charging a ' +
231
+ 'stored token on your own schedule. Tokenize with `storePaymentMethod: true`, then ' +
232
+ 'call `charge({ paymentMethodId })` each cycle from your own scheduler.');
233
+ }
234
+ async cancelSubscription(_subscriptionGatewayId, _options) {
235
+ throw new Error('[payments] Adyen has no subscription to cancel — nothing is scheduled at the ' +
236
+ 'gateway. Stop charging the token, and delete it with ' +
237
+ '`DELETE /v71/storedPaymentMethods/{id}` if the shopper asked you to.');
238
+ }
239
+ async updateSubscription(_subscriptionGatewayId, _input) {
240
+ throw new Error('[payments] Adyen has no subscription to update — the amount of the next charge is ' +
241
+ 'whatever your scheduler sends to `charge()`.');
242
+ }
243
+ async findSubscription(_gatewayId) {
244
+ throw new Error('[payments] Adyen has no subscription resource to look up. The recurring state you ' +
245
+ 'have is the stored token plus your own billing rows.');
246
+ }
247
+ // ── Invoices ─────────────────────────────────────────────────────────────────────────
248
+ async listInvoices(_customerId) {
249
+ throw new Error('[payments] Adyen Checkout has no invoice resource. Configure an `invoice` provider ' +
250
+ 'and pass `invoice: true` on the charge if you need a fiscal document.');
251
+ }
252
+ // ── Disputes ─────────────────────────────────────────────────────────────────────────
253
+ /**
254
+ * Adyen has no endpoint that reads a dispute back, so this refuses instead of inventing
255
+ * one.
256
+ *
257
+ * Defend Disputes v30 is five POSTs — `retrieveApplicableDefenseReasons`,
258
+ * `supplyDefenseDocument`, `defendDispute`, `acceptDispute`,
259
+ * `deleteDisputeDefenseDocument` — and every one of them is an action. None returns the
260
+ * dispute's status, amount, reason or deadline, which means a `Dispute` built here would
261
+ * have those fields invented.
262
+ */
263
+ async findDispute(_disputeGatewayId) {
264
+ throw new Error('[payments] Adyen has no endpoint that reads a dispute back. Defend Disputes v30 ' +
265
+ 'exposes only actions (retrieveApplicableDefenseReasons, supplyDefenseDocument, ' +
266
+ 'defendDispute, acceptDispute, deleteDisputeDefenseDocument) — none of them ' +
267
+ 'returns the dispute — so its status, amount and deadline would be invented here. ' +
268
+ 'The dispute you have is the webhook: NOTIFICATION_OF_CHARGEBACK carries the ' +
269
+ 'reason, the amount and `additionalData.defensePeriodEndsAt`, which this driver ' +
270
+ 'already surfaces as `payment.dispute_warning` with `actionableUntil`. Store that ' +
271
+ 'row; the Customer Area dispute report is the only other source.');
272
+ }
273
+ /**
274
+ * A defense at Adyen cannot be expressed as a {@link DisputeEvidence}, so this refuses
275
+ * and says what to call instead.
276
+ *
277
+ * Adyen's flow is `retrieveApplicableDefenseReasons` → `supplyDefenseDocument` (once per
278
+ * document) → `defendDispute`, and it turns on a **`defenseReasonCode`** the scheme
279
+ * defines per reason code — there is no free-text evidence object anywhere in it. The
280
+ * shared shape has no field for that code, no way to carry a document's bytes
281
+ * (`documentIds` are ids Adyen never issues; v30 takes base64 `content` plus
282
+ * `contentType` and `defenseDocumentTypeCode` inline), and no home at all for
283
+ * `explanation`, `receiptUrl` or the customer fields. Mapping it anyway would drop the
284
+ * defense reason, which is the entire defense.
285
+ *
286
+ * Two more things a mapping would have to paper over, both real: the API lives on a
287
+ * different host from Checkout v71 (`ca-{test,live}.adyen.com/ca/services/DisputeService
288
+ * /v30`, which this driver's `#request` does not reach), and it needs an API credential
289
+ * with the "API dispute management" role, which the Checkout key usually is not.
290
+ */
291
+ async submitDisputeEvidence(_disputeGatewayId, _evidence) {
292
+ throw new Error('[payments] Adyen cannot be sent a `DisputeEvidence`. Defending a dispute there is ' +
293
+ 'three POSTs to https://ca-test.adyen.com/ca/services/DisputeService/v30 ' +
294
+ '(`ca-live` in production): retrieveApplicableDefenseReasons, then ' +
295
+ 'supplyDefenseDocument for each required document, then defendDispute. It requires ' +
296
+ 'a scheme-specific `defenseReasonCode` and documents as base64 `content` + ' +
297
+ '`contentType` + `defenseDocumentTypeCode`, and it has no free-text evidence field ' +
298
+ 'at all — so any mapping from `DisputeEvidence` would drop the defense reason and ' +
299
+ 'submit a defense the scheme rejects. Call the Disputes API directly, with an API ' +
300
+ 'credential holding the "API dispute management" role.');
301
+ }
302
+ // ── Webhooks ─────────────────────────────────────────────────────────────────────────
303
+ /**
304
+ * Verify Adyen's HMAC and normalize the delivery into one event per notification item.
305
+ *
306
+ * The signature covers eight fields of the notification item, colon-joined in a fixed
307
+ * order with empty strings for the absent ones, HMAC-SHA256 under the **hex-decoded**
308
+ * Customer Area key, base64-encoded, and delivered inside the item's `additionalData`.
309
+ * Adyen's own Node, PHP, Java and Python libraries all join those fields *without*
310
+ * escaping — the `\` → `\\`, `:` → `\:` rule belongs to the classic HPP/dictionary
311
+ * signature, not to this one — so this does the same, and a merchant reference
312
+ * containing a colon signs identically here and at Adyen.
313
+ *
314
+ * **The signature is PER ITEM, not per request.** Adyen's reference is explicit that for
315
+ * standard webhooks the signature is provided inside each notification's `additionalData`
316
+ * (only some other webhook families sign the request as a whole, in a header), and its own
317
+ * PHP sample loops the array calling `isValidNotificationHMAC` on every item. Nothing
318
+ * signs the envelope, so an attacker holding one genuine item can append any items they
319
+ * like beside it — verifying the first and trusting the rest would accept every one of
320
+ * them. Every item is verified, before any of them is mapped, and one bad signature
321
+ * rejects the whole delivery: mapping is pointless work once the body is known to be
322
+ * partly forged, and a 400 is the honest answer to a request that was tampered with.
323
+ *
324
+ * **Batches.** Adyen documents that JSON and HTTP POST webhooks carry a single
325
+ * notification item, and that only the legacy SOAP transport may batch (up to six items
326
+ * when events land in rapid succession). This driver speaks JSON, so the array is one item
327
+ * in practice — but it is an array in the schema, so the driver reads all of it and
328
+ * returns one {@link WebhookEvent} per item. A single item still returns a single event,
329
+ * not an array of one.
330
+ */
331
+ /**
332
+ * Whether a delivery to `POST /payments/webhook/:provider` can be authenticated.
333
+ *
334
+ * Without the HMAC key every `notificationItem` is taken on trust.
335
+ */
336
+ get webhookVerification() {
337
+ return this.#hmacKey !== undefined ? 'configured' : 'unconfigured';
338
+ }
339
+ parseWebhook(rawBody, _headers) {
340
+ const payload = JSON.parse(rawBody);
341
+ const items = payload.notificationItems ?? [];
342
+ if (items.length === 0) {
343
+ throw new Error('[payments] Adyen webhook carried no notificationItems.');
344
+ }
345
+ // Pass one: authenticate everything. Kept separate from the mapping below so that no
346
+ // future edit can reorder its way into building events out of unverified items.
347
+ if (this.#hmacKey !== undefined) {
348
+ for (const entry of items) {
349
+ this.#verifyHmac(entry.NotificationRequestItem, this.#hmacKey);
350
+ }
351
+ }
352
+ // Pass two: normalize. Each item becomes an event with its own id, so the ledger gives
353
+ // each one its own row and a redelivery re-runs only what has not been processed.
354
+ const events = items.map((entry) => this.#mapNotification(entry.NotificationRequestItem, payload));
355
+ return events.length === 1 ? events[0] : events;
356
+ }
357
+ /** One `NotificationRequestItem` → one normalized event. Assumes it has been verified. */
358
+ #mapNotification(item, payload) {
359
+ // A modification (CAPTURE, REFUND, CANCELLATION) carries its own pspReference and
360
+ // names the payment in `originalReference` — the payment is what the ledger routes on.
361
+ const paymentReference = item.originalReference ?? item.pspReference ?? '';
362
+ const success = item.success === 'true';
363
+ const type = this.#mapWebhookType(item.eventCode, success, item.additionalData);
364
+ return {
365
+ id: `adyen:${item.eventCode}:${item.pspReference ?? paymentReference}`,
366
+ provider: this.provider,
367
+ type,
368
+ ...(item.eventDate !== undefined ? { createdAt: item.eventDate } : {}),
369
+ data: {
370
+ gatewayId: paymentReference,
371
+ ...(item.amount !== undefined
372
+ ? { amount: item.amount.value, currency: item.amount.currency.toLowerCase() }
373
+ : {}),
374
+ ...(item.merchantReference !== undefined
375
+ ? { externalReference: item.merchantReference }
376
+ : {}),
377
+ ...(item.reason !== undefined && item.reason !== '' ? { reason: item.reason } : {}),
378
+ ...this.#disputeExtras(item, type),
379
+ },
380
+ // The envelope narrowed to THIS item, rather than the whole delivery. The ledger
381
+ // stores `raw` per event, and a row holding all four notifications could not say
382
+ // which one it is about — while this shape is still a valid Adyen payload, so the
383
+ // dashboard can replay the row. Identical to the delivery body for a single item.
384
+ raw: {
385
+ ...payload,
386
+ notificationItems: [{ NotificationRequestItem: item }],
387
+ },
388
+ };
389
+ }
390
+ // ── Mappers ──────────────────────────────────────────────────────────────────────────
391
+ #mapPayment(data, requested, currency, customerId, requestedMethodType) {
392
+ const result = {
393
+ id: data.pspReference ?? '',
394
+ gatewayId: data.pspReference ?? '',
395
+ provider: this.provider,
396
+ amount: data.amount
397
+ ? { amount: data.amount.value, currency: data.amount.currency.toLowerCase() }
398
+ : { amount: requested, currency: (currency ?? this.#currency).toLowerCase() },
399
+ status: this.#mapResultCode(data.resultCode),
400
+ // Adyen echoes the settled instrument as `additionalData.paymentMethod` ('visa',
401
+ // 'ideal', 'sepadirectdebit'); when it does not, the type the request asked for is
402
+ // the next best truth. Hardcoding `card` here labelled a SEPA mandate or an iDEAL
403
+ // transfer a card payment, which the ledger cannot tell apart from a real one.
404
+ method: this.#mapPaymentMethodType(data.additionalData?.paymentMethod ?? requestedMethodType),
405
+ payload: data,
406
+ // Adyen's payment response has no timestamp of its own.
407
+ createdAt: new Date().toISOString(),
408
+ };
409
+ if (customerId !== undefined)
410
+ result.customerId = customerId;
411
+ return result;
412
+ }
413
+ /**
414
+ * An Adyen payment-method type or brand → the canonical {@link PaymentMethodType}.
415
+ *
416
+ * By category, not by brand. Adyen fronts something like a hundred local methods and
417
+ * adds more every quarter; `PaymentMethodType` is a closed union precisely so a routing
418
+ * typo fails at the manager, and it can only stay closed if it names categories. The
419
+ * brand survives verbatim on `payment.payload.additionalData.paymentMethod`.
420
+ */
421
+ #mapPaymentMethodType(type) {
422
+ const name = type.toLowerCase();
423
+ // `scheme` is Adyen's word for "a card"; the brands are what `additionalData` echoes.
424
+ if (name === 'scheme' ||
425
+ ['visa', 'mc', 'amex', 'discover', 'diners', 'jcb', 'maestro', 'cup', 'elo'].includes(name)) {
426
+ return name === 'maestro' ? 'debit_card' : 'card';
427
+ }
428
+ if (name.includes('directdebit') || name === 'ach' || name === 'sepadirectdebit') {
429
+ return 'bank_debit';
430
+ }
431
+ if (['ideal', 'bcmc', 'eps', 'trustly', 'multibanco', 'mbway', 'blik', 'przelewy24'].includes(name) ||
432
+ name.startsWith('onlinebanking') ||
433
+ name.startsWith('directebanking')) {
434
+ return 'bank_transfer';
435
+ }
436
+ if (['paypal', 'applepay', 'googlepay', 'paywithgoogle', 'alipay', 'twint', 'vipps'].includes(name) ||
437
+ name.startsWith('wechatpay')) {
438
+ return 'wallet';
439
+ }
440
+ if (name.startsWith('klarna') || name.startsWith('afterpay') || name === 'clearpay') {
441
+ return 'bnpl';
442
+ }
443
+ if (name === 'paysafecard' || name === 'econtext_stores')
444
+ return 'voucher';
445
+ return 'unknown';
446
+ }
447
+ /**
448
+ * What `Authorised` means depends on the account, and the Checkout API will not say.
449
+ *
450
+ * On an automatic-capture account (Adyen's default) the capture follows the
451
+ * authorization on its own and no CAPTURE webhook is sent, so `Authorised` is the last
452
+ * word the driver will ever get about that money: it reads as `paid`. On a
453
+ * **manual-capture** account it is a hold that expires unless you capture it, and the
454
+ * CAPTURE webhook is what settles it — so it reads as `authorized`, the state that says
455
+ * "the funds are reserved and nothing has moved".
456
+ *
457
+ * `captureMode` is configuration for exactly that reason: `captureDelay` is a Management
458
+ * API field, not a Checkout one, and both modes answer `/payments` identically.
459
+ */
460
+ #mapResultCode(resultCode) {
461
+ switch (resultCode) {
462
+ case 'Authorised':
463
+ return this.#captureMode === 'manual' ? 'authorized' : 'paid';
464
+ case 'Refused':
465
+ case 'Error':
466
+ return 'failed';
467
+ case 'Cancelled':
468
+ return 'canceled';
469
+ default:
470
+ return 'pending';
471
+ }
472
+ }
473
+ /**
474
+ * An Adyen `eventCode` → the canonical event type.
475
+ *
476
+ * AUTHORISATION is the money event on an automatic-capture account: the capture happens
477
+ * on its own and Adyen sends **no** CAPTURE webhook, so treating the authorization as
478
+ * "still pending" would leave every payment on such an account unsettled forever. On a
479
+ * manual-capture account it is only a hold, and CAPTURE is the settlement — so which of
480
+ * the two means `payment.succeeded` follows `captureMode`, the same knob
481
+ * {@link AdyenDriver.#mapResultCode} reads.
482
+ */
483
+ #mapWebhookType(eventCode, success, additional) {
484
+ switch (eventCode) {
485
+ case 'AUTHORISATION':
486
+ if (!success)
487
+ return 'payment.failed';
488
+ // Manual capture: the funds are held, not taken. There is deliberately no
489
+ // `payment.authorized` event, so this is an update until CAPTURE arrives.
490
+ return this.#captureMode === 'manual' ? 'payment.updated' : 'payment.succeeded';
491
+ case 'CAPTURE':
492
+ return success ? 'payment.succeeded' : 'payment.failed';
493
+ case 'AUTHORISATION_ADJUSTMENT':
494
+ case 'CAPTURE_FAILED':
495
+ return success ? 'payment.updated' : 'payment.failed';
496
+ case 'REFUND':
497
+ // A failed refund left the payment where it was; only a successful one refunds it.
498
+ return success ? 'payment.refunded' : 'payment.updated';
499
+ // ── The dispute family ───────────────────────────────────────────────────────────
500
+ // Three notifications where Adyen has withdrawn NOTHING yet. A NOTIFICATION_OF_FRAUD
501
+ // is the issuer's TC40/SAFE alert, a REQUEST_FOR_INFORMATION is the scheme asking a
502
+ // question, and a NOTIFICATION_OF_CHARGEBACK is a chargeback announced but not yet
503
+ // taken — Adyen's own table lists "funds withdrawn: no" for all three. Calling any of
504
+ // them `payment.disputed` moves a paid row over money that is still in the account.
505
+ //
506
+ // NOTIFICATION_OF_CHARGEBACK is also where `defensePeriodEndsAt` arrives, and that
507
+ // deadline is the only thing that makes a dispute actionable: flattening it into an
508
+ // event with no room for a deadline threw away the field the operator needs.
509
+ case 'NOTIFICATION_OF_FRAUD':
510
+ case 'REQUEST_FOR_INFORMATION':
511
+ case 'NOTIFICATION_OF_CHARGEBACK':
512
+ return 'payment.dispute_warning';
513
+ // The money is gone. NOTIFICATION_OF_CHARGEBACK does not always precede it — an ACH
514
+ // return goes straight here with no notification and cannot be defended at all — so
515
+ // this stands alone rather than assuming a warning arrived first.
516
+ case 'CHARGEBACK':
517
+ return 'payment.disputed';
518
+ // Defended and the amount returned. Adyen does not call this final, because
519
+ // pre-arbitration can still follow with a second close carrying the opposite
520
+ // outcome — but the alternative is emitting nothing for the outcome operators care
521
+ // about most.
522
+ case 'CHARGEBACK_REVERSED':
523
+ case 'PREARBITRATION_WON':
524
+ return 'payment.dispute_closed';
525
+ // The issuing bank declined the material submitted during defense, and pre-arbitration
526
+ // declined. Both are final and both leave the money with the cardholder.
527
+ case 'SECOND_CHARGEBACK':
528
+ case 'PREARBITRATION_LOST':
529
+ return 'payment.dispute_closed';
530
+ // "Defense period expired or liability accepted" — the outcome is in `disputeStatus`,
531
+ // not in the event code. Without a status this driver will not guess which, so it
532
+ // stays an update rather than reporting a loss that might be a win.
533
+ case 'DISPUTE_DEFENSE_PERIOD_ENDED':
534
+ return this.#disputeOutcome(additional) === undefined
535
+ ? 'payment.updated'
536
+ : 'payment.dispute_closed';
537
+ case 'REFUND_FAILED':
538
+ case 'REFUNDED_REVERSED':
539
+ case 'CANCELLATION':
540
+ case 'CANCEL_OR_REFUND':
541
+ // Defense documents uploaded, or Adyen auto-defended. Movement inside an open
542
+ // dispute, not a resolution of it.
543
+ case 'INFORMATION_SUPPLIED':
544
+ return 'payment.updated';
545
+ default:
546
+ return eventCode.toLowerCase();
547
+ }
548
+ }
549
+ /**
550
+ * Adyen reports the result of a dispute in `additionalData.disputeStatus`, not in the
551
+ * event code, for the one event that can mean either — `DISPUTE_DEFENSE_PERIOD_ENDED` is
552
+ * "expired or liability accepted". `Accepted` and `Undefended` are losses: you did not
553
+ * defend, so the cardholder keeps the money. Anything unrecognized returns `undefined`
554
+ * and the caller stays on `payment.updated` rather than reporting a result.
555
+ */
556
+ #disputeOutcome(additional) {
557
+ switch (additional?.disputeStatus) {
558
+ case 'Won':
559
+ return 'won';
560
+ case 'Lost':
561
+ case 'Accepted':
562
+ case 'Undefended':
563
+ return 'lost';
564
+ case 'Expired':
565
+ return 'expired';
566
+ default:
567
+ return undefined;
568
+ }
569
+ }
570
+ /**
571
+ * The fields that make a dispute event actionable, added only to the two types that have
572
+ * room for them. On a dispute notification Adyen's `pspReference` is the DISPUTE's
573
+ * reference and `originalReference` is the payment's — which is why `gatewayId` above
574
+ * reads the latter and the dispute id reads the former.
575
+ */
576
+ #disputeExtras(item, type) {
577
+ if (type === 'payment.dispute_warning') {
578
+ const endsAt = item.additionalData?.defensePeriodEndsAt;
579
+ return {
580
+ ...(item.pspReference !== undefined ? { disputeId: item.pspReference } : {}),
581
+ // The whole value of the alert. Adyen sends it as ISO 8601 with an offset.
582
+ ...(endsAt !== undefined ? { actionableUntil: endsAt } : {}),
583
+ };
584
+ }
585
+ if (type === 'payment.dispute_closed') {
586
+ const outcome = this.#closedOutcome(item);
587
+ return {
588
+ ...(item.pspReference !== undefined ? { disputeId: item.pspReference } : {}),
589
+ ...(outcome !== undefined ? { outcome } : {}),
590
+ };
591
+ }
592
+ return {};
593
+ }
594
+ /** The event code names the outcome, except for the one that does not. */
595
+ #closedOutcome(item) {
596
+ switch (item.eventCode) {
597
+ case 'CHARGEBACK_REVERSED':
598
+ case 'PREARBITRATION_WON':
599
+ return 'won';
600
+ case 'SECOND_CHARGEBACK':
601
+ case 'PREARBITRATION_LOST':
602
+ return 'lost';
603
+ default:
604
+ return this.#disputeOutcome(item.additionalData);
605
+ }
606
+ }
607
+ // ── Helpers ──────────────────────────────────────────────────────────────────────────
608
+ /**
609
+ * Adyen's `amount.value` is already an integer in the currency's minor units — the same
610
+ * unit this package uses — so it passes straight through. The neighbouring Mollie driver
611
+ * converts to a decimal string; the difference is Adyen's, not an oversight here.
612
+ */
613
+ #amount(amount, currency) {
614
+ return { value: amount, currency: (currency ?? this.#currency).toUpperCase() };
615
+ }
616
+ #verifyHmac(item, hmacKey) {
617
+ const received = item.additionalData?.hmacSignature;
618
+ if (received === undefined || received === '') {
619
+ throw new Error('[payments] Missing hmacSignature on Adyen webhook notification.');
620
+ }
621
+ const signedData = [
622
+ item.pspReference,
623
+ item.originalReference,
624
+ item.merchantAccountCode,
625
+ item.merchantReference,
626
+ item.amount?.value,
627
+ item.amount?.currency,
628
+ item.eventCode,
629
+ item.success,
630
+ ]
631
+ .map((value) => (value === undefined || value === null ? '' : String(value)))
632
+ .join(':');
633
+ // The Customer Area key is hex; signing with its characters instead of its bytes is
634
+ // the classic way to get a signature that never matches.
635
+ const expected = createHmac('sha256', Buffer.from(hmacKey, 'hex'))
636
+ .update(signedData, 'utf8')
637
+ .digest('base64');
638
+ if (!safeCompare(received, expected)) {
639
+ throw new Error('[payments] Invalid Adyen webhook HMAC signature.');
640
+ }
641
+ }
642
+ /**
643
+ * Adyen deduplicates on the `Idempotency-Key` request header and nowhere else; it never
644
+ * echoes the key back on the response, so this header is the whole mechanism. Adyen's
645
+ * reference recommends a UUID and caps it at 64 characters.
646
+ */
647
+ #idempotency(key) {
648
+ if (key === undefined)
649
+ return {};
650
+ if (key.length > IDEMPOTENCY_MAX_LENGTH) {
651
+ // Adyen rejects the request outright rather than ignoring the header, so failing here
652
+ // costs a round trip and names the real limit instead of a 422 about a header.
653
+ throw new Error(`[payments] Adyen caps \`Idempotency-Key\` at ${IDEMPOTENCY_MAX_LENGTH} characters; got ${key.length}.`);
654
+ }
655
+ return { headers: { 'Idempotency-Key': key } };
656
+ }
657
+ /** Split the Adyen-specific knobs out of `metadata`; the rest is echoed as metadata. */
658
+ #passthrough(metadata) {
659
+ const { paymentMethodType, shopperInteraction, recurringProcessingModel, returnUrl, ...rest } = metadata ?? {};
660
+ return {
661
+ ...(typeof paymentMethodType === 'string' ? { paymentMethodType } : {}),
662
+ ...(typeof shopperInteraction === 'string' ? { shopperInteraction } : {}),
663
+ ...(typeof recurringProcessingModel === 'string' ? { recurringProcessingModel } : {}),
664
+ ...(typeof returnUrl === 'string' ? { returnUrl } : {}),
665
+ rest,
666
+ };
667
+ }
668
+ async #request(path, options = {}) {
669
+ return httpRequest(path, {
670
+ baseUrl: this.#baseUrl,
671
+ ...(options.method !== undefined ? { method: options.method } : {}),
672
+ ...(options.body !== undefined ? { body: options.body } : {}),
673
+ ...(options.headers !== undefined ? { headers: options.headers } : {}),
674
+ authHeader: { name: 'X-API-Key', value: this.#apiKey },
675
+ });
676
+ }
677
+ }
678
+ //# sourceMappingURL=adyen.js.map