@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,844 @@
1
+ import { createHmac, randomUUID } 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 { fromDecimal, toDecimal } from '../money.js';
6
+ import { safeCompare } from '../webhook_security.js';
7
+ import { requireCredential, requireCurrency } from './shared.js';
8
+ /** Mercado Pago rejects an `external_reference` outside this shape (max 64 chars). */
9
+ const EXTERNAL_REFERENCE_PATTERN = /^[A-Za-z0-9_-]{1,64}$/;
10
+ /**
11
+ * Mercado Pago driver — the largest gateway in Latin America, over the Checkout API
12
+ * (`/v1/payments`), Checkout Pro preferences and preapproval subscriptions. Plain REST,
13
+ * no SDK.
14
+ *
15
+ * Unlike the BRL-only Brazilian drivers this one is multi-currency across BR/AR/MX/CL/
16
+ * CO/PE/UY, so `currency` is required. Money crosses the boundary as a decimal
17
+ * `transaction_amount`.
18
+ *
19
+ * The one thing to know before reading {@link parseWebhook}: a Mercado Pago notification
20
+ * carries **only the id of the resource that changed**. Nothing else — no amount, no
21
+ * status, no `external_reference`.
22
+ */
23
+ export class MercadoPagoDriver {
24
+ provider = 'mercadopago';
25
+ /**
26
+ * What `charge()` actually creates. `undefined` is absent on purpose: Mercado Pago
27
+ * requires a `payment_method_id` on every payment, so "let the customer choose" is the
28
+ * Checkout Pro flow ({@link createCheckout}), not a charge.
29
+ */
30
+ supportedMethods = ['pix', 'boleto', 'credit_card', 'debit_card'];
31
+ capabilities = { refunds: true, invoices: false, subscriptions: true };
32
+ #baseUrl = 'https://api.mercadopago.com';
33
+ #accessToken;
34
+ #currency;
35
+ #webhookSecret;
36
+ #invoiceCtx;
37
+ constructor(ctx, config) {
38
+ this.#invoiceCtx = ctx;
39
+ this.#accessToken = requireCredential({
40
+ driver: 'mercadopago',
41
+ option: 'accessToken',
42
+ env: 'MERCADOPAGO_ACCESS_TOKEN',
43
+ value: config.accessToken,
44
+ });
45
+ this.#currency = requireCurrency('mercadopago', config.currency);
46
+ this.#webhookSecret = config.webhookSecret ?? process.env.MERCADOPAGO_WEBHOOK_SECRET;
47
+ }
48
+ // ── Customers ────────────────────────────────────────────────────────────────────────
49
+ async createCustomer(input) {
50
+ this.#refuseIdempotencyKey(input.idempotencyKey, 'createCustomer', 'POST /v1/customers');
51
+ const [firstName, ...rest] = (input.name ?? '').split(' ');
52
+ const body = {
53
+ ...(input.email !== undefined ? { email: input.email } : {}),
54
+ ...(firstName !== undefined && firstName !== '' ? { first_name: firstName } : {}),
55
+ ...(rest.length > 0 ? { last_name: rest.join(' ') } : {}),
56
+ ...(input.taxId !== undefined ? { identification: this.#identification(input.taxId) } : {}),
57
+ ...(input.metadata !== undefined ? input.metadata : {}),
58
+ };
59
+ const data = await this.#request('/v1/customers', {
60
+ method: 'POST',
61
+ body,
62
+ });
63
+ return this.#mapCustomer(data);
64
+ }
65
+ async findCustomer(customerId) {
66
+ try {
67
+ const data = await this.#request(`/v1/customers/${customerId}`);
68
+ return this.#mapCustomer(data);
69
+ }
70
+ catch (error) {
71
+ if (isNotFound(error))
72
+ return null;
73
+ throw error;
74
+ }
75
+ }
76
+ async updateCustomer(customerId, input) {
77
+ const [firstName, ...rest] = (input.name ?? '').split(' ');
78
+ const body = {
79
+ // Mercado Pago only accepts `email` on a customer that has none yet; sending it for
80
+ // one that already has an email comes back as error 126.
81
+ ...(input.email !== undefined ? { email: input.email } : {}),
82
+ ...(firstName !== undefined && firstName !== '' ? { first_name: firstName } : {}),
83
+ ...(rest.length > 0 ? { last_name: rest.join(' ') } : {}),
84
+ ...(input.taxId !== undefined ? { identification: this.#identification(input.taxId) } : {}),
85
+ ...(input.metadata !== undefined ? input.metadata : {}),
86
+ };
87
+ const data = await this.#request(`/v1/customers/${customerId}`, {
88
+ method: 'PUT',
89
+ body,
90
+ });
91
+ return this.#mapCustomer(data);
92
+ }
93
+ // ── Payments ─────────────────────────────────────────────────────────────────────────
94
+ async charge(input) {
95
+ const currency = input.currency ?? this.#currency;
96
+ const token = input.card?.token ?? input.paymentMethodId;
97
+ const paymentMethodId = this.#paymentMethodId(input, token !== undefined);
98
+ if (input.externalReference !== undefined) {
99
+ this.#assertExternalReference(input.externalReference);
100
+ }
101
+ const body = {
102
+ transaction_amount: this.#toAmount(input.amount, currency),
103
+ payment_method_id: paymentMethodId,
104
+ payer: this.#payer(input),
105
+ ...(input.description !== undefined ? { description: input.description } : {}),
106
+ ...(input.externalReference !== undefined
107
+ ? { external_reference: input.externalReference }
108
+ : {}),
109
+ ...(token !== undefined
110
+ ? {
111
+ token,
112
+ // Mercado Pago rejects a card payment with no `installments` (error 4004);
113
+ // one is the single-payment case, not a guess at the shopper's choice.
114
+ installments: this.#numberOption(input.metadata?.installments) ?? 1,
115
+ ...(typeof input.metadata?.issuerId === 'string'
116
+ ? { issuer_id: input.metadata.issuerId }
117
+ : {}),
118
+ }
119
+ : {}),
120
+ ...(typeof input.metadata?.dateOfExpiration === 'string'
121
+ ? { date_of_expiration: input.metadata.dateOfExpiration }
122
+ : {}),
123
+ ...(typeof input.metadata?.notificationUrl === 'string'
124
+ ? { notification_url: input.metadata.notificationUrl }
125
+ : {}),
126
+ ...(input.metadata !== undefined ? { metadata: input.metadata } : {}),
127
+ };
128
+ const data = await this.#request('/v1/payments', {
129
+ method: 'POST',
130
+ body,
131
+ // Mercado Pago requires the header on every payment. A key the caller did not give
132
+ // us cannot deduplicate their retry — pass `idempotencyKey` to actually get that.
133
+ idempotencyKey: input.idempotencyKey ?? randomUUID(),
134
+ });
135
+ const payment = this.#mapPayment(data, currency);
136
+ await emitInvoiceIfRequested(this.#invoiceCtx, input, payment, this);
137
+ publishPaymentDiagnostics(payment);
138
+ return payment;
139
+ }
140
+ async findPayment(gatewayId) {
141
+ try {
142
+ const data = await this.#request(`/v1/payments/${gatewayId}`);
143
+ return this.#mapPayment(data, this.#currency);
144
+ }
145
+ catch (error) {
146
+ if (isNotFound(error))
147
+ return null;
148
+ throw error;
149
+ }
150
+ }
151
+ async refund(paymentGatewayId, amount, options) {
152
+ const currency = this.#currency;
153
+ // No `amount` is a full refund, per Mercado Pago's own wording.
154
+ const body = amount !== undefined ? { amount: this.#toAmount(amount, currency) } : {};
155
+ const data = await this.#request(`/v1/payments/${paymentGatewayId}/refunds`, {
156
+ method: 'POST',
157
+ body,
158
+ // The header is required here too, so a caller's key finally reaches it. The
159
+ // generated fallback stays for the callers who pass none, and it is deliberately
160
+ // random rather than derived from the payment id: two partial refunds of the same
161
+ // amount are a normal thing to want, and a per-payment key would collapse the
162
+ // second into the first and report a success for a refund never made.
163
+ idempotencyKey: options?.idempotencyKey ?? randomUUID(),
164
+ });
165
+ const refund = {
166
+ id: String(data.id),
167
+ gatewayId: String(data.id),
168
+ provider: this.provider,
169
+ amount: { amount: this.#fromAmount(data.amount, currency), currency },
170
+ status: data.status === 'approved'
171
+ ? 'succeeded'
172
+ : data.status === 'rejected' || data.status === 'cancelled'
173
+ ? 'failed'
174
+ : 'pending',
175
+ createdAt: data.date_created ?? new Date().toISOString(),
176
+ };
177
+ publishRefundDiagnostics(refund);
178
+ return refund;
179
+ }
180
+ // ── Checkout ─────────────────────────────────────────────────────────────────────────
181
+ /**
182
+ * Checkout Pro: a hosted preference the payer completes on Mercado Pago, choosing the
183
+ * method there. `back_urls` must be https — Mercado Pago silently discards http ones.
184
+ */
185
+ async createCheckout(input) {
186
+ const currency = input.currency ?? this.#currency;
187
+ const externalReference = input.externalReference;
188
+ if (externalReference !== undefined)
189
+ this.#assertExternalReference(externalReference);
190
+ const body = {
191
+ items: [
192
+ {
193
+ title: input.description ?? 'Payment',
194
+ quantity: 1,
195
+ currency_id: currency.toUpperCase(),
196
+ unit_price: this.#toAmount(input.amount, currency),
197
+ },
198
+ ],
199
+ back_urls: {
200
+ success: input.successUrl,
201
+ pending: input.successUrl,
202
+ ...(input.cancelUrl !== undefined ? { failure: input.cancelUrl } : {}),
203
+ },
204
+ auto_return: 'approved',
205
+ ...(externalReference !== undefined ? { external_reference: externalReference } : {}),
206
+ ...(input.metadata !== undefined ? { metadata: input.metadata } : {}),
207
+ };
208
+ const data = await this.#request('/checkout/preferences', {
209
+ method: 'POST',
210
+ body,
211
+ });
212
+ return {
213
+ id: data.id,
214
+ gatewayId: data.id,
215
+ provider: this.provider,
216
+ // `sandbox_init_point` is documented as "do not use" — test credentials go through
217
+ // `init_point` too.
218
+ url: data.init_point ?? '',
219
+ status: 'open',
220
+ amount: { amount: input.amount, currency },
221
+ ...(input.customerId !== undefined ? { customerId: input.customerId } : {}),
222
+ };
223
+ }
224
+ // ── Subscriptions ────────────────────────────────────────────────────────────────────
225
+ /**
226
+ * A preapproval. With an `amount` the driver creates a subscription with an inline
227
+ * recurrence; without one, `planId` is used as an existing `preapproval_plan_id` and the
228
+ * plan defines the price and the cycle.
229
+ */
230
+ async createSubscription(input) {
231
+ this.#refuseIdempotencyKey(input.idempotencyKey, 'createSubscription', 'POST /preapproval');
232
+ const currency = this.#currency;
233
+ if (input.externalReference !== undefined) {
234
+ this.#assertExternalReference(input.externalReference);
235
+ }
236
+ const endDate = typeof input.metadata?.endDate === 'string' ? input.metadata.endDate : undefined;
237
+ // Mercado Pago documents that `start_date` is only honoured alongside `end_date`.
238
+ // Sending it alone would start the billing on a date the gateway silently ignores.
239
+ if (input.startDate !== undefined && endDate === undefined) {
240
+ throw new Error('[payments] Mercado Pago ignores a subscription `start_date` unless an end date is ' +
241
+ 'sent with it. Pass `metadata.endDate` (ISO) alongside `startDate`, or drop ' +
242
+ '`startDate` and let it start now.');
243
+ }
244
+ const email = input.customer?.email ?? input.card?.holder?.email;
245
+ const body = {
246
+ ...(input.amount !== undefined
247
+ ? {
248
+ reason: input.description ?? 'Subscription',
249
+ auto_recurring: {
250
+ ...this.#recurrence(input.cycle),
251
+ transaction_amount: this.#toAmount(input.amount, currency),
252
+ currency_id: currency.toUpperCase(),
253
+ ...(input.startDate !== undefined ? { start_date: input.startDate } : {}),
254
+ ...(endDate !== undefined ? { end_date: endDate } : {}),
255
+ ...(input.trialDays !== undefined
256
+ ? { free_trial: { frequency: input.trialDays, frequency_type: 'days' } }
257
+ : {}),
258
+ },
259
+ }
260
+ : { preapproval_plan_id: input.planId }),
261
+ ...(email !== undefined ? { payer_email: email } : {}),
262
+ ...(input.card !== undefined ? { card_token_id: input.card.token } : {}),
263
+ ...(input.externalReference !== undefined
264
+ ? { external_reference: input.externalReference }
265
+ : {}),
266
+ ...(typeof input.metadata?.backUrl === 'string' ? { back_url: input.metadata.backUrl } : {}),
267
+ // `authorized` needs a payment method; without a card the payer authorizes it at the
268
+ // `init_point` and Mercado Pago flips it itself.
269
+ status: input.card !== undefined ? 'authorized' : 'pending',
270
+ };
271
+ if (input.amount !== undefined && body.back_url === undefined) {
272
+ throw new Error('[payments] Mercado Pago requires `back_url` on a subscription created without a ' +
273
+ 'plan — pass `metadata.backUrl`, or create a preapproval plan and pass its id as ' +
274
+ '`planId` with no `amount`.');
275
+ }
276
+ const data = await this.#request('/preapproval', {
277
+ method: 'POST',
278
+ body,
279
+ });
280
+ const subscription = this.#mapSubscription(data);
281
+ publishSubscriptionDiagnostics(subscription, 'subscription.created');
282
+ return subscription;
283
+ }
284
+ async cancelSubscription(subscriptionGatewayId, options) {
285
+ if (options?.atPeriodEnd === true) {
286
+ throw new Error('[payments] Mercado Pago cancels a preapproval immediately and irreversibly — there ' +
287
+ 'is no cancel-at-period-end flag. Pause it instead (`status: "paused"`) and cancel ' +
288
+ 'when the period ends, or keep the grace period on your own records.');
289
+ }
290
+ const data = await this.#request(`/preapproval/${subscriptionGatewayId}`,
291
+ // Preapproval spells it `canceled` with one L, unlike a payment's `cancelled`.
292
+ { method: 'PUT', body: { status: 'canceled' } });
293
+ const subscription = this.#mapSubscription(data);
294
+ publishSubscriptionDiagnostics(subscription, 'subscription.canceled');
295
+ return subscription;
296
+ }
297
+ async updateSubscription(subscriptionGatewayId, input) {
298
+ this.#refuseIdempotencyKey(input.idempotencyKey, 'updateSubscription', 'PUT /preapproval/{id}');
299
+ const currency = this.#currency;
300
+ const body = {
301
+ ...(input.amount !== undefined
302
+ ? {
303
+ auto_recurring: {
304
+ transaction_amount: this.#toAmount(input.amount, currency),
305
+ currency_id: currency.toUpperCase(),
306
+ },
307
+ }
308
+ : {}),
309
+ // `reason` is the subscription's own description, the one the payer sees.
310
+ ...(input.description !== undefined ? { reason: input.description } : {}),
311
+ };
312
+ const data = await this.#request(`/preapproval/${subscriptionGatewayId}`, { method: 'PUT', body });
313
+ return this.#mapSubscription(data);
314
+ }
315
+ async findSubscription(gatewayId) {
316
+ try {
317
+ const data = await this.#request(`/preapproval/${gatewayId}`);
318
+ return this.#mapSubscription(data);
319
+ }
320
+ catch (error) {
321
+ if (isNotFound(error))
322
+ return null;
323
+ throw error;
324
+ }
325
+ }
326
+ // ── Invoices ─────────────────────────────────────────────────────────────────────────
327
+ async listInvoices(_customerId) {
328
+ throw new Error('[payments] Mercado Pago has no invoices for a customer. `GET /authorized_payments/' +
329
+ 'search` lists the charges of a *subscription* and filters by `payer_id`, which is a ' +
330
+ 'Mercado Pago user id, not the `/v1/customers` id. Use `findPayment`, or an invoice ' +
331
+ 'provider with `invoice: true`.');
332
+ }
333
+ // ── Webhooks ─────────────────────────────────────────────────────────────────────────
334
+ /**
335
+ * Verify the `x-signature` HMAC, then fetch the resource the notification names.
336
+ *
337
+ * A Mercado Pago notification is an id and nothing more — no amount, no status, no
338
+ * `external_reference` — so the fetch is not an optimization to skip: without it this
339
+ * could only ever return `payment.updated`, and a payment that settled would be
340
+ * ledgered and never marked paid. It costs one API call per notification, which is
341
+ * Mercado Pago's design rather than an inefficiency of this driver.
342
+ *
343
+ * A failed fetch throws, so the mounted route answers 400 and Mercado Pago retries.
344
+ * Reporting a status the gateway did not confirm would be worse than a retry.
345
+ */
346
+ /**
347
+ * Whether a delivery to `POST /payments/webhook/:provider` can be authenticated.
348
+ *
349
+ * Without the secret `#verify` hands the payload back unchecked.
350
+ */
351
+ get webhookVerification() {
352
+ return this.#webhookSecret !== undefined ? 'configured' : 'unconfigured';
353
+ }
354
+ async parseWebhook(rawBody, headers) {
355
+ const payload = this.#verified(rawBody, headers);
356
+ const type = payload.type ?? '';
357
+ const resourceId = payload.data?.id === undefined ? '' : String(payload.data.id);
358
+ const event = {
359
+ id: payload.id !== undefined ? String(payload.id) : `${payload.action ?? type}-${resourceId}`,
360
+ provider: this.provider,
361
+ type: this.#mapWebhookType(type),
362
+ createdAt: payload.date_created ?? new Date().toISOString(),
363
+ data: { gatewayId: resourceId },
364
+ raw: payload,
365
+ };
366
+ if (resourceId === '')
367
+ return event;
368
+ if (type === 'payment') {
369
+ return this.#paymentEvent(event, resourceId);
370
+ }
371
+ // A chargeback case: opened, or its status changed. Mercado Pago sends one action for
372
+ // both (`actions: ["changed_case_status"]`), so the notification itself cannot say
373
+ // whether this is the opening — the payment can, and it is the payment this library
374
+ // has a row for. `data.id` here is the CASE id (`GET /v1/chargebacks/{id}`), so the
375
+ // payment is read from `data.payment_id`; without it there is no payment to dispute
376
+ // and the event stays the `payment.updated` above rather than filing one under a case
377
+ // id that matches nothing.
378
+ if (type === 'topic_chargebacks_wh') {
379
+ const paymentId = payload.data?.payment_id;
380
+ if (paymentId === undefined)
381
+ return event;
382
+ return this.#withChargebackDetails(await this.#paymentEvent(event, String(paymentId)), {
383
+ caseId: resourceId,
384
+ });
385
+ }
386
+ if (type === 'subscription_preapproval') {
387
+ const preapproval = await this.#request(`/preapproval/${resourceId}`);
388
+ const subscription = this.#mapSubscription(preapproval);
389
+ return {
390
+ ...event,
391
+ type: preapproval.status === 'canceled' ? 'subscription.canceled' : 'subscription.updated',
392
+ data: {
393
+ gatewayId: subscription.gatewayId,
394
+ customerId: subscription.customerId,
395
+ status: subscription.status,
396
+ planId: subscription.planId,
397
+ ...(preapproval.external_reference !== undefined
398
+ ? { externalReference: preapproval.external_reference }
399
+ : {}),
400
+ },
401
+ };
402
+ }
403
+ // `subscription_authorized_payment` (a subscription's recurring charge) is left as
404
+ // `payment.updated` with its id: it is read from `/authorized_payments/{id}`, whose
405
+ // response shape this driver has not verified against the reference. Guessing the
406
+ // field names of a settled charge is exactly the mistake that ships wrong money.
407
+ return event;
408
+ }
409
+ /**
410
+ * Fetch a payment and turn it into the webhook event for whatever happened to it.
411
+ *
412
+ * Shared by the `payment` topic (where the notification names the payment) and the
413
+ * chargeback topic (where it names the case and the payment separately), so a dispute
414
+ * is normalized through exactly the same status mapping as everything else.
415
+ */
416
+ async #paymentEvent(event, paymentId) {
417
+ const payment = await this.#request(`/v1/payments/${paymentId}`);
418
+ const currency = payment.currency_id?.toLowerCase() ?? this.#currency;
419
+ const type = this.#paymentEventType(payment.status, payment.status_detail);
420
+ const outcome = type === 'payment.dispute_closed' ? this.#disputeOutcome(payment.status_detail) : undefined;
421
+ return {
422
+ ...event,
423
+ type,
424
+ data: {
425
+ gatewayId: String(payment.id),
426
+ amount: this.#fromAmount(payment.transaction_amount, currency),
427
+ currency,
428
+ ...(payment.payer?.id !== undefined ? { customerId: String(payment.payer.id) } : {}),
429
+ ...(payment.external_reference !== undefined
430
+ ? { externalReference: payment.external_reference }
431
+ : {}),
432
+ // The close has to carry its result: the processor throws on a
433
+ // `payment.dispute_closed` with no outcome rather than inventing one.
434
+ ...(outcome !== undefined ? { outcome } : {}),
435
+ },
436
+ };
437
+ }
438
+ /**
439
+ * Add the chargeback CASE's own fields to a dispute event.
440
+ *
441
+ * Only `topic_chargebacks_wh` reaches this, because only that notification names a case
442
+ * (`data.id`). The extra `GET /v1/chargebacks/{id}` buys `date_documentation_deadline` —
443
+ * the evidence deadline, and the single field that makes a Mercado Pago dispute
444
+ * actionable — plus the case's `reason`.
445
+ *
446
+ * It **fails soft**, unlike the payment fetch. By the time this runs the money question
447
+ * is already answered from the payment, and throwing here would turn a chargeback the
448
+ * driver read correctly into a 400 and a redelivery. A missing deadline costs an
449
+ * operator context; a dropped chargeback costs the row.
450
+ */
451
+ async #withChargebackDetails(event, { caseId }) {
452
+ if (event.type !== 'payment.disputed' && event.type !== 'payment.dispute_closed')
453
+ return event;
454
+ const data = { ...event.data, disputeId: caseId };
455
+ let chargeback;
456
+ try {
457
+ chargeback = await this.#request(`/v1/chargebacks/${caseId}`);
458
+ }
459
+ catch {
460
+ return { ...event, data };
461
+ }
462
+ const deadline = chargeback.date_documentation_deadline;
463
+ return {
464
+ ...event,
465
+ data: {
466
+ ...data,
467
+ ...(typeof chargeback.reason === 'string' && chargeback.reason !== ''
468
+ ? { reason: chargeback.reason }
469
+ : {}),
470
+ // ISO 8601 with an offset, e.g. `2024-10-24T12:48:24.000-04:00`. `null` whenever
471
+ // `documentation_required` is false — there is nothing to defend, so no clock.
472
+ ...(typeof deadline === 'string' && deadline !== '' ? { actionableUntil: deadline } : {}),
473
+ },
474
+ };
475
+ }
476
+ // ── Mappers ──────────────────────────────────────────────────────────────────────────
477
+ /**
478
+ * Check the `x-signature` HMAC and return the parsed notification.
479
+ *
480
+ * The manifest Mercado Pago signs is `id:<data.id>;request-id:<x-request-id>;ts:<ts>;`,
481
+ * with any absent part left out. The `id` in it is the `data.id` **query parameter**,
482
+ * which `parseWebhook` never sees — the driver contract passes the body and the headers
483
+ * and nothing else — so the same id is read from the body, where Mercado Pago puts it
484
+ * too. The docs say to lowercase an alphanumeric id while the official SDK hashes
485
+ * whatever it is handed, so both spellings are accepted.
486
+ */
487
+ #verified(rawBody, headers) {
488
+ const payload = JSON.parse(rawBody);
489
+ if (this.#webhookSecret === undefined)
490
+ return payload;
491
+ const header = headerValue(headers, 'x-signature');
492
+ if (header === undefined || header === '') {
493
+ throw new Error('[payments] Missing `x-signature` header on Mercado Pago webhook request.');
494
+ }
495
+ let timestamp;
496
+ let received;
497
+ for (const part of header.split(',')) {
498
+ const separator = part.indexOf('=');
499
+ if (separator === -1)
500
+ continue;
501
+ const key = part.slice(0, separator).trim().toLowerCase();
502
+ const value = part.slice(separator + 1).trim();
503
+ if (key === 'ts')
504
+ timestamp = value;
505
+ else if (key === 'v1')
506
+ received = value;
507
+ }
508
+ if (timestamp === undefined || received === undefined) {
509
+ throw new Error('[payments] Malformed `x-signature` header on Mercado Pago webhook.');
510
+ }
511
+ const requestId = headerValue(headers, 'x-request-id');
512
+ const dataId = payload.data?.id === undefined ? undefined : String(payload.data.id);
513
+ const candidates = dataId !== undefined && dataId !== dataId.toLowerCase()
514
+ ? [dataId.toLowerCase(), dataId]
515
+ : [dataId];
516
+ const matched = candidates.some((candidate) => safeCompare(received, this.#signature(candidate, requestId, timestamp)));
517
+ if (!matched) {
518
+ throw new Error('[payments] Invalid Mercado Pago webhook signature.');
519
+ }
520
+ return payload;
521
+ }
522
+ #signature(dataId, requestId, timestamp) {
523
+ const parts = [];
524
+ if (dataId !== undefined && dataId !== '')
525
+ parts.push(`id:${dataId}`);
526
+ if (requestId !== undefined && requestId !== '')
527
+ parts.push(`request-id:${requestId}`);
528
+ parts.push(`ts:${timestamp}`);
529
+ return createHmac('sha256', this.#webhookSecret ?? '')
530
+ .update(`${parts.join(';')};`)
531
+ .digest('hex');
532
+ }
533
+ #mapWebhookType(type) {
534
+ switch (type) {
535
+ // Both carry a payment id and neither says what happened to it.
536
+ case 'payment':
537
+ case 'subscription_authorized_payment':
538
+ return 'payment.updated';
539
+ case 'subscription_preapproval':
540
+ return 'subscription.updated';
541
+ // Deliberately not `payment.disputed`: the notification carries the chargeback case
542
+ // id, not the payment id, and the only honest dispute event is the one built after
543
+ // the payment named by `data.payment_id` has been fetched. This is what the topic
544
+ // degrades to when that id is missing.
545
+ case 'topic_chargebacks_wh':
546
+ return 'payment.updated';
547
+ default:
548
+ return type === '' ? 'unknown' : type.toLowerCase();
549
+ }
550
+ }
551
+ #paymentEventType(status, statusDetail) {
552
+ switch (status) {
553
+ case 'approved':
554
+ return 'payment.succeeded';
555
+ case 'rejected':
556
+ case 'cancelled':
557
+ return 'payment.failed';
558
+ case 'refunded':
559
+ return 'payment.refunded';
560
+ // A card chargeback, and `status_detail` says where it stands. Mercado Pago's own
561
+ // table: `in_process` is "chargeback received, the dispute is in progress, awaiting a
562
+ // final decision"; `settled` is "decision against the seller, money withdrawn from
563
+ // the seller's account"; `reimbursed` is "decision in favor of the seller, money
564
+ // refunded to the seller's account". So the two terminal details are the outcome, and
565
+ // reading them is the difference between a dispute that closes and one that sits at
566
+ // `disputed` forever — a WON dispute left there writes off money that came back,
567
+ // because `revenue()` only sums `paid`.
568
+ //
569
+ // Note what the reference does NOT say: whether the funds leave the account at
570
+ // `in_process` or only at `settled`. Mercado Pago describes `settled` as the
571
+ // withdrawal, but the payment's top-level status is already `charged_back` and the
572
+ // guides describe the amount as retained from the opening. Since the reference will
573
+ // not settle it, this stays `payment.disputed` — the mapping it already had — rather
574
+ // than being downgraded to a warning on a guess.
575
+ case 'charged_back':
576
+ return this.#disputeOutcome(statusDetail) === undefined
577
+ ? 'payment.disputed'
578
+ : 'payment.dispute_closed';
579
+ // A claim opened inside Mercado Pago rather than at the issuer. The reference says
580
+ // only "users have initiated a dispute" and does not say whether the money is
581
+ // withheld, so this keeps the `payment.disputed` it has always had; a claim has no
582
+ // documented `status_detail` outcome to close on either.
583
+ case 'in_mediation':
584
+ return 'payment.disputed';
585
+ default:
586
+ return 'payment.updated';
587
+ }
588
+ }
589
+ /**
590
+ * The outcome of a `charged_back` payment, read from `status_detail`.
591
+ *
592
+ * `in_process` — and anything unrecognized — returns `undefined`, which keeps the event
593
+ * at `payment.disputed`. The dispute is open; there is no result to report, and inventing
594
+ * one would close a row over money still being argued about.
595
+ *
596
+ * `coverage_applied` on the chargeback case says the same thing (`true` favours the
597
+ * seller), but it is on a resource the `payment` topic never names, so the payment's own
598
+ * detail is what this reads.
599
+ */
600
+ #disputeOutcome(statusDetail) {
601
+ if (statusDetail === 'reimbursed')
602
+ return 'won';
603
+ if (statusDetail === 'settled')
604
+ return 'lost';
605
+ return undefined;
606
+ }
607
+ #mapCustomer(data) {
608
+ const name = [data.first_name, data.last_name].filter(Boolean).join(' ');
609
+ return {
610
+ id: String(data.id),
611
+ ...(data.email !== undefined ? { email: data.email } : {}),
612
+ ...(name !== '' ? { name } : {}),
613
+ ...(data.identification?.number !== undefined ? { taxId: data.identification.number } : {}),
614
+ };
615
+ }
616
+ #mapPayment(data, fallbackCurrency) {
617
+ const statusMap = {
618
+ approved: 'paid',
619
+ // The card was held (`status_detail: pending_capture`) and nothing was captured —
620
+ // the money has NOT moved. It used to collapse into `pending`, which understates a
621
+ // hold the issuer already granted; `paid` would have granted access against money
622
+ // that can still evaporate when the authorization expires uncaptured.
623
+ authorized: 'authorized',
624
+ pending: 'pending',
625
+ in_process: 'pending',
626
+ in_mediation: 'disputed',
627
+ rejected: 'failed',
628
+ cancelled: 'canceled',
629
+ refunded: 'refunded',
630
+ charged_back: 'disputed',
631
+ };
632
+ const currency = data.currency_id?.toLowerCase() ?? fallbackCurrency;
633
+ const method = this.#mapMethodToType(data.payment_method_id, data.payment_type_id);
634
+ const result = {
635
+ id: String(data.id),
636
+ gatewayId: String(data.id),
637
+ provider: this.provider,
638
+ amount: { amount: this.#fromAmount(data.transaction_amount, currency), currency },
639
+ status: statusMap[data.status ?? ''] ?? 'pending',
640
+ payload: data,
641
+ createdAt: data.date_created ?? new Date().toISOString(),
642
+ };
643
+ if (method !== 'unknown')
644
+ result.method = method;
645
+ if (data.payer?.id !== undefined)
646
+ result.customerId = String(data.payer.id);
647
+ if (data.date_approved !== undefined)
648
+ result.paidAt = data.date_approved;
649
+ const pix = data.point_of_interaction?.transaction_data;
650
+ if (pix?.qr_code !== undefined) {
651
+ result.pixCode = pix.qr_code;
652
+ result.pixCopiaECola = pix.qr_code;
653
+ }
654
+ if (pix?.qr_code_base64 !== undefined) {
655
+ result.pixQrCodeImage = pix.qr_code_base64;
656
+ result.pixQrCode = pix.qr_code_base64;
657
+ }
658
+ const hostedUrl = pix?.ticket_url ?? data.transaction_details?.external_resource_url;
659
+ if (hostedUrl !== undefined)
660
+ result.hostedUrl = hostedUrl;
661
+ return result;
662
+ }
663
+ #mapSubscription(data) {
664
+ const statusMap = {
665
+ authorized: 'active',
666
+ // The payer has not authorized it yet — nothing has been charged.
667
+ pending: 'incomplete',
668
+ // Billing has stopped and the preapproval is still alive — it can be resumed, and
669
+ // it must not entitle the payer meanwhile. It used to map to `past_due`, which says
670
+ // a charge failed; nothing failed here, Mercado Pago was told to stop.
671
+ paused: 'paused',
672
+ canceled: 'canceled',
673
+ };
674
+ const currency = data.auto_recurring?.currency_id?.toLowerCase() ?? this.#currency;
675
+ return {
676
+ id: data.id,
677
+ gatewayId: data.id,
678
+ provider: this.provider,
679
+ customerId: data.payer_id === undefined ? '' : String(data.payer_id),
680
+ status: statusMap[data.status ?? ''] ?? 'incomplete',
681
+ planId: data.preapproval_plan_id ?? data.reason ?? '',
682
+ ...(data.auto_recurring?.transaction_amount !== undefined
683
+ ? {
684
+ amount: {
685
+ amount: this.#fromAmount(data.auto_recurring.transaction_amount, currency),
686
+ currency,
687
+ },
688
+ }
689
+ : {}),
690
+ ...(data.auto_recurring?.start_date !== undefined
691
+ ? { currentPeriodStart: data.auto_recurring.start_date }
692
+ : {}),
693
+ ...(data.next_payment_date !== undefined ? { currentPeriodEnd: data.next_payment_date } : {}),
694
+ ...(data.auto_recurring?.end_date !== undefined
695
+ ? { endsAt: data.auto_recurring.end_date }
696
+ : {}),
697
+ payload: data,
698
+ createdAt: data.date_created ?? new Date().toISOString(),
699
+ };
700
+ }
701
+ /** `WEEKLY` and friends onto the only two frequency types Mercado Pago accepts. */
702
+ #recurrence(cycle) {
703
+ switch (cycle) {
704
+ case 'WEEKLY':
705
+ return { frequency: 7, frequency_type: 'days' };
706
+ case 'BIWEEKLY':
707
+ return { frequency: 14, frequency_type: 'days' };
708
+ case 'QUARTERLY':
709
+ return { frequency: 3, frequency_type: 'months' };
710
+ case 'SEMIANNUALLY':
711
+ return { frequency: 6, frequency_type: 'months' };
712
+ case 'YEARLY':
713
+ return { frequency: 12, frequency_type: 'months' };
714
+ default:
715
+ return { frequency: 1, frequency_type: 'months' };
716
+ }
717
+ }
718
+ /**
719
+ * The `payment_method_id` for this charge. Pix and boleto have fixed ids; a card is
720
+ * identified by its brand (`visa`, `master`, `debvisa`…), which the tokenizer in the
721
+ * frontend returns alongside the token and only the caller can know.
722
+ */
723
+ #paymentMethodId(input, hasToken) {
724
+ if (typeof input.metadata?.paymentMethodId === 'string')
725
+ return input.metadata.paymentMethodId;
726
+ switch (input.method) {
727
+ case 'pix':
728
+ return 'pix';
729
+ case 'boleto':
730
+ return 'bolbradesco';
731
+ case 'credit_card':
732
+ case 'debit_card':
733
+ throw new Error('[payments] Mercado Pago identifies a card payment by its brand, not by "card". ' +
734
+ 'Pass the `payment_method_id` the card tokenizer returned (e.g. "visa", "master", ' +
735
+ '"debvisa") as `metadata.paymentMethodId`.');
736
+ default:
737
+ throw new Error(hasToken
738
+ ? '[payments] Mercado Pago needs the card brand for a tokenized charge — pass it ' +
739
+ 'as `metadata.paymentMethodId` (e.g. "visa").'
740
+ : '[payments] Mercado Pago requires a payment method on every charge. Pass ' +
741
+ '`method: "pix"` or `"boleto"`, or use `createCheckout()` to let the payer ' +
742
+ 'choose on Checkout Pro.');
743
+ }
744
+ }
745
+ #payer(input) {
746
+ const holder = input.card?.holder;
747
+ const email = input.customer?.email ?? holder?.email;
748
+ const name = input.customer?.name ?? holder?.name;
749
+ const taxId = input.customer?.taxId ?? holder?.cpfCnpj;
750
+ const [firstName, ...rest] = (name ?? '').split(' ');
751
+ return {
752
+ ...(input.customerId !== undefined ? { id: input.customerId, type: 'customer' } : {}),
753
+ ...(email !== undefined ? { email } : {}),
754
+ ...(firstName !== undefined && firstName !== '' ? { first_name: firstName } : {}),
755
+ ...(rest.length > 0 ? { last_name: rest.join(' ') } : {}),
756
+ ...(taxId !== undefined ? { identification: this.#identification(taxId) } : {}),
757
+ // The reference documents the payer address under `additional_info.payer.address`
758
+ // while the Pix and boleto guides nest it here; boleto rejects a payment without it.
759
+ // The guides are the ones with working end-to-end examples.
760
+ ...(typeof input.metadata?.address === 'object' && input.metadata.address !== null
761
+ ? { address: input.metadata.address }
762
+ : {}),
763
+ };
764
+ }
765
+ /**
766
+ * A tax id and its Mercado Pago `identification.type`. Only Brazil's CPF/CNPJ can be
767
+ * told apart by length; every other country has its own document types, so those must
768
+ * be passed explicitly.
769
+ */
770
+ #identification(taxId) {
771
+ const digits = taxId.replace(/\D/g, '');
772
+ return { type: digits.length > 11 ? 'CNPJ' : 'CPF', number: digits };
773
+ }
774
+ #assertExternalReference(value) {
775
+ if (!EXTERNAL_REFERENCE_PATTERN.test(value)) {
776
+ throw new Error(`[payments] Mercado Pago rejects the externalReference "${value}". It allows at most ` +
777
+ `64 characters — letters, numbers, hyphens and underscores — and got ${value.length}.`);
778
+ }
779
+ }
780
+ #mapMethodToType(paymentMethodId, paymentTypeId) {
781
+ if (paymentMethodId?.toLowerCase() === 'pix')
782
+ return 'pix';
783
+ switch (paymentTypeId) {
784
+ case 'credit_card':
785
+ case 'prepaid_card':
786
+ return 'card';
787
+ case 'debit_card':
788
+ return 'debit_card';
789
+ case 'ticket':
790
+ return 'boleto';
791
+ default:
792
+ return 'unknown';
793
+ }
794
+ }
795
+ /**
796
+ * `X-Idempotency-Key` is documented — and mandatory — on `POST /v1/payments` and
797
+ * `POST /v1/payments/{id}/refunds`, and on nothing else: the reference for
798
+ * `/v1/customers` and `/preapproval` lists `Authorization` and no more. Sending it
799
+ * there anyway would be harmless and unspecified, which is the worst combination — the
800
+ * caller would believe their retry is safe. So those operations refuse it.
801
+ */
802
+ #refuseIdempotencyKey(key, operation, endpoint) {
803
+ if (key === undefined)
804
+ return;
805
+ throw new Error(`[payments] Mercado Pago documents \`X-Idempotency-Key\` only for payments and refunds, so \`${operation}\` cannot honour an idempotencyKey — its \`${endpoint}\` reference documents no such header. \`charge()\` and \`refund()\` do honour it; for the rest, deduplicate before you call (a preapproval can be looked up by \`external_reference\`).`);
806
+ }
807
+ #numberOption(value) {
808
+ return typeof value === 'number' ? value : undefined;
809
+ }
810
+ // ── Money ────────────────────────────────────────────────────────────────────────────
811
+ /**
812
+ * Minor units → the decimal `transaction_amount` Mercado Pago wants (a JSON number, not
813
+ * a string). The currency has to travel with it: CLP has no cents, so dividing a
814
+ * Chilean amount by 100 bills 1% of it — and Mercado Pago accepts that happily.
815
+ *
816
+ * COP is not a special case here: ISO 4217 gives it two minor units even though
817
+ * Colombian prices are quoted in whole pesos. Check a COP charge in sandbox.
818
+ */
819
+ #toAmount(amount, currency) {
820
+ return toDecimal(amount, currency);
821
+ }
822
+ /** Mercado Pago's decimal amount → minor units. It sometimes quotes them as strings. */
823
+ #fromAmount(value, currency) {
824
+ const parsed = typeof value === 'string' ? Number(value) : (value ?? 0);
825
+ if (Number.isNaN(parsed))
826
+ return 0;
827
+ return fromDecimal(parsed, currency);
828
+ }
829
+ // ── Transport ────────────────────────────────────────────────────────────────────────
830
+ async #request(path, options = {}) {
831
+ return httpRequest(path, {
832
+ baseUrl: this.#baseUrl,
833
+ bearerToken: this.#accessToken,
834
+ ...(options.method !== undefined ? { method: options.method } : {}),
835
+ ...(options.body !== undefined ? { body: options.body } : {}),
836
+ // Mercado Pago's idempotency key is a header — written into the body it would
837
+ // deduplicate nothing.
838
+ ...(options.idempotencyKey !== undefined
839
+ ? { headers: { 'X-Idempotency-Key': options.idempotencyKey } }
840
+ : {}),
841
+ });
842
+ }
843
+ }
844
+ //# sourceMappingURL=mercadopago.js.map