@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,632 @@
1
+ import { createHmac } from 'node:crypto';
2
+ import { publishRefundDiagnostics, publishSubscriptionDiagnostics } from '../diagnostics.js';
3
+ import { headerValue, isNotFound } from '../http.js';
4
+ import { safeCompare } from '../webhook_security.js';
5
+ import { requireCredential } from './shared.js';
6
+ /**
7
+ * Lemon Squeezy driver — the v1 REST API on `api.lemonsqueezy.com`.
8
+ *
9
+ * Lemon Squeezy is a merchant of record: it is the seller of record on the buyer's
10
+ * statement, calculates and remits sales tax/VAT worldwide, and pays you out. You sell to
11
+ * Lemon Squeezy; Lemon Squeezy sells to your customer.
12
+ *
13
+ * Two consequences shape this driver. First, there is no server-side charge endpoint at
14
+ * all — a purchase begins with a hosted checkout, so `charge()` throws and
15
+ * `createCheckout()` is the entry point. Second, subscriptions are created by a customer
16
+ * completing a checkout for a subscription variant, never by an API call, so
17
+ * `createSubscription()` throws too.
18
+ *
19
+ * The API is JSON:API (`application/vnd.api+json`), which is unusual enough that the
20
+ * request helper and every mapper below are written around `data.attributes`.
21
+ */
22
+ export class LemonSqueezyDriver {
23
+ provider = 'lemonsqueezy';
24
+ /**
25
+ * The hosted checkout decides which methods to offer from the buyer's country and the
26
+ * store's settings, and the API takes no payment-method argument — so `'undefined'`
27
+ * ("the customer chooses at checkout") is the only honest canonical name. Routing
28
+ * `credit_card` here is refused by the manager, which is correct: this driver cannot
29
+ * promise the payment will be a card rather than PayPal.
30
+ */
31
+ supportedMethods = ['undefined'];
32
+ /**
33
+ * `disputes: false`. Lemon Squeezy has no dispute API and no dispute webhook — see
34
+ * `#mapWebhookType`. As merchant of record it "typically manages these disputes on
35
+ * behalf of the seller", so there is nothing here to read and nothing to submit.
36
+ */
37
+ capabilities = { refunds: true, invoices: true, subscriptions: true, disputes: false };
38
+ #baseUrl = 'https://api.lemonsqueezy.com/v1';
39
+ #apiKey;
40
+ #storeId;
41
+ #webhookSecret;
42
+ constructor(_ctx, config) {
43
+ this.#apiKey = requireCredential({
44
+ driver: 'lemonsqueezy',
45
+ option: 'apiKey',
46
+ env: 'LEMONSQUEEZY_API_KEY',
47
+ value: config.apiKey,
48
+ });
49
+ this.#storeId = String(config.storeId);
50
+ // An empty env var is "unset", not "a secret that can never match" — otherwise a
51
+ // blank `LEMONSQUEEZY_WEBHOOK_SECRET` fails every webhook with a signature error
52
+ // instead of the message telling you to configure it.
53
+ const webhookSecret = config.webhookSecret ?? process.env.LEMONSQUEEZY_WEBHOOK_SECRET;
54
+ this.#webhookSecret = webhookSecret === '' ? undefined : webhookSecret;
55
+ }
56
+ // ── Customers ────────────────────────────────────────────────────────────────────────
57
+ async createCustomer(input) {
58
+ this.#refuseIdempotencyKey(input.idempotencyKey, 'createCustomer');
59
+ if (input.name === undefined || input.email === undefined) {
60
+ throw new Error('[payments] Lemon Squeezy requires both a name and an email on a customer.');
61
+ }
62
+ this.#refuseTaxId(input.taxId);
63
+ const document = await this.#request('/customers', {
64
+ method: 'POST',
65
+ body: {
66
+ data: {
67
+ type: 'customers',
68
+ attributes: {
69
+ name: input.name,
70
+ email: input.email,
71
+ ...(typeof input.metadata?.city === 'string' ? { city: input.metadata.city } : {}),
72
+ ...(typeof input.metadata?.region === 'string'
73
+ ? { region: input.metadata.region }
74
+ : {}),
75
+ ...(typeof input.metadata?.country === 'string'
76
+ ? { country: input.metadata.country }
77
+ : {}),
78
+ },
79
+ relationships: { store: { data: { type: 'stores', id: this.#storeId } } },
80
+ },
81
+ },
82
+ });
83
+ return this.#mapCustomer(document.data);
84
+ }
85
+ async findCustomer(customerId) {
86
+ try {
87
+ const document = await this.#request(`/customers/${encodeURIComponent(customerId)}`);
88
+ return this.#mapCustomer(document.data);
89
+ }
90
+ catch (error) {
91
+ if (isNotFound(error))
92
+ return null;
93
+ throw error;
94
+ }
95
+ }
96
+ async updateCustomer(customerId, input) {
97
+ this.#refuseTaxId(input.taxId);
98
+ const document = await this.#request(`/customers/${encodeURIComponent(customerId)}`, {
99
+ method: 'PATCH',
100
+ body: {
101
+ data: {
102
+ type: 'customers',
103
+ id: String(customerId),
104
+ attributes: {
105
+ ...(input.name !== undefined ? { name: input.name } : {}),
106
+ ...(input.email !== undefined ? { email: input.email } : {}),
107
+ },
108
+ },
109
+ },
110
+ });
111
+ return this.#mapCustomer(document.data);
112
+ }
113
+ // ── Payments ─────────────────────────────────────────────────────────────────────────
114
+ /**
115
+ * Lemon Squeezy has no endpoint that takes money. As merchant of record it owns the
116
+ * payment page: the only way to start a purchase is a hosted checkout, so returning a
117
+ * `Payment` here would report a charge that was never attempted.
118
+ */
119
+ async charge(_input) {
120
+ throw new Error('[payments] Lemon Squeezy cannot charge server-side — as merchant of record it ' +
121
+ 'collects every payment through its hosted checkout. Use `createCheckout()` and ' +
122
+ 'act on the `order_created` webhook.');
123
+ }
124
+ async findPayment(gatewayId) {
125
+ try {
126
+ const document = await this.#request(`/orders/${encodeURIComponent(gatewayId)}`);
127
+ return this.#mapPayment(document.data);
128
+ }
129
+ catch (error) {
130
+ if (isNotFound(error))
131
+ return null;
132
+ throw error;
133
+ }
134
+ }
135
+ /**
136
+ * Refunds an **order** (`paymentGatewayId` is an order id, the same id `findPayment`
137
+ * takes). `amount` is in cents, like everywhere else here; omit it for a full refund.
138
+ * Renewal charges are subscription invoices with their own ids and their own refund
139
+ * endpoint — this method does not reach them.
140
+ *
141
+ * `options.idempotencyKey` is REFUSED, not ignored: Lemon Squeezy has no deduplication
142
+ * mechanism, so accepting a key would turn a retry guarantee into a second refund.
143
+ */
144
+ async refund(paymentGatewayId, amount, options) {
145
+ this.#refuseIdempotencyKey(options?.idempotencyKey, 'refund');
146
+ const document = await this.#request(`/orders/${encodeURIComponent(paymentGatewayId)}/refund`, {
147
+ method: 'POST',
148
+ body: {
149
+ data: {
150
+ type: 'orders',
151
+ id: String(paymentGatewayId),
152
+ attributes: { ...(amount !== undefined ? { amount } : {}) },
153
+ },
154
+ },
155
+ });
156
+ const order = document.data.attributes;
157
+ const refund = {
158
+ id: document.data.id,
159
+ gatewayId: document.data.id,
160
+ provider: this.provider,
161
+ amount: { amount: amount ?? order.total, currency: order.currency.toLowerCase() },
162
+ // The refund endpoint returns the order, not a refund resource: `refunded` is the
163
+ // only confirmation the API gives that the money went back.
164
+ status: order.refunded === true || order.status === 'refunded' ? 'succeeded' : 'pending',
165
+ createdAt: order.refunded_at ?? new Date().toISOString(),
166
+ };
167
+ publishRefundDiagnostics(refund);
168
+ return refund;
169
+ }
170
+ // ── Checkout ─────────────────────────────────────────────────────────────────────────
171
+ /**
172
+ * Creates a hosted checkout for a **variant** — `planId` is a Lemon Squeezy variant id
173
+ * and is required, because a checkout always sells something from the catalog. Pass
174
+ * `amount` to override the variant's price (`custom_price`, in cents).
175
+ */
176
+ async createCheckout(input) {
177
+ this.#refuseIdempotencyKey(input.idempotencyKey, 'createCheckout');
178
+ if (input.planId === undefined) {
179
+ throw new Error('[payments] Lemon Squeezy checkouts always sell a catalog variant. Pass the ' +
180
+ 'variant id as `planId` (use `amount` to override its price).');
181
+ }
182
+ if (input.trialDays !== undefined) {
183
+ // A Lemon Squeezy trial is a property of the variant, not of the checkout.
184
+ // Accepting the option here would bill a customer who was promised a trial.
185
+ throw new Error('[payments] Lemon Squeezy configures trials on the variant, not per checkout. ' +
186
+ 'Create a variant with a free trial and pass its id as `planId`.');
187
+ }
188
+ const custom = this.#customData(input);
189
+ const document = await this.#request('/checkouts', {
190
+ method: 'POST',
191
+ body: {
192
+ data: {
193
+ type: 'checkouts',
194
+ attributes: {
195
+ // Lemon Squeezy's money is an integer of the smallest currency unit, the
196
+ // same unit this package works in — `custom_price: 1990` is $19.90. No
197
+ // `formatDecimal`, no division. (API reference, the checkout object:
198
+ // "a positive integer in cents representing the custom price".)
199
+ ...(input.amount > 0 ? { custom_price: input.amount } : {}),
200
+ product_options: {
201
+ ...(input.description !== undefined ? { name: input.description } : {}),
202
+ // Lemon Squeezy has one post-purchase URL, not a success/cancel pair.
203
+ redirect_url: input.successUrl,
204
+ },
205
+ checkout_data: {
206
+ ...(typeof input.metadata?.email === 'string'
207
+ ? { email: input.metadata.email }
208
+ : {}),
209
+ ...(typeof input.metadata?.name === 'string' ? { name: input.metadata.name } : {}),
210
+ ...(typeof input.metadata?.taxNumber === 'string'
211
+ ? { tax_number: input.metadata.taxNumber }
212
+ : {}),
213
+ ...(custom !== undefined ? { custom } : {}),
214
+ },
215
+ },
216
+ relationships: {
217
+ store: { data: { type: 'stores', id: this.#storeId } },
218
+ variant: { data: { type: 'variants', id: String(input.planId) } },
219
+ },
220
+ },
221
+ },
222
+ });
223
+ const attributes = document.data.attributes;
224
+ return {
225
+ id: document.data.id,
226
+ gatewayId: document.data.id,
227
+ provider: this.provider,
228
+ url: attributes.url,
229
+ status: 'open',
230
+ // No `amount`: the checkout response states a price but never a currency (the store
231
+ // sets it), and this package will not invent one. The currency arrives on the
232
+ // `order_created` webhook, where it is read off the order.
233
+ ...(input.customerId !== undefined ? { customerId: input.customerId } : {}),
234
+ };
235
+ }
236
+ // ── Subscriptions ────────────────────────────────────────────────────────────────────
237
+ /**
238
+ * Lemon Squeezy's API has no create-subscription endpoint: a subscription exists only
239
+ * once a customer has completed a checkout for a subscription variant. Anything
240
+ * returned here would be a subscription Lemon Squeezy has never heard of.
241
+ */
242
+ async createSubscription(_input) {
243
+ throw new Error('[payments] Lemon Squeezy cannot create a subscription over the API — it creates ' +
244
+ 'one when a customer completes a checkout for a subscription variant. Call ' +
245
+ "`createCheckout({ planId: '<variant id>' })` and read the id off the " +
246
+ '`subscription_created` webhook.');
247
+ }
248
+ async cancelSubscription(subscriptionGatewayId, options) {
249
+ if (options?.atPeriodEnd === false) {
250
+ // Cancelling puts the subscription in `cancelled` and lets it run to `ends_at`;
251
+ // there is no immediate-termination endpoint, and pretending otherwise would leave
252
+ // a customer with access the caller believes was revoked.
253
+ throw new Error('[payments] Lemon Squeezy cancels at the end of the current billing period and ' +
254
+ 'has no immediate cancellation. Call `cancelSubscription(id)` without ' +
255
+ '`atPeriodEnd: false`, and revoke access yourself on `ends_at`.');
256
+ }
257
+ const document = await this.#request(`/subscriptions/${encodeURIComponent(subscriptionGatewayId)}`, { method: 'DELETE' });
258
+ const subscription = this.#mapSubscription(document.data);
259
+ publishSubscriptionDiagnostics(subscription, 'subscription.canceled');
260
+ return subscription;
261
+ }
262
+ /**
263
+ * The only change Lemon Squeezy accepts on a subscription that this contract can carry
264
+ * is a plan swap, and it is expressed as a variant id — so it is read from
265
+ * `metadata.variantId`. There is no writable amount (the price belongs to the variant),
266
+ * no description, and no custom-data field; each is refused rather than dropped.
267
+ */
268
+ async updateSubscription(subscriptionGatewayId, input) {
269
+ this.#refuseIdempotencyKey(input.idempotencyKey, 'updateSubscription');
270
+ if (input.amount !== undefined) {
271
+ throw new Error('[payments] Lemon Squeezy has no editable amount on a subscription — the price ' +
272
+ 'belongs to the variant. Swap plans with ' +
273
+ "`updateSubscription(id, { metadata: { variantId: '<variant id>' } })`.");
274
+ }
275
+ if (input.description !== undefined) {
276
+ throw new Error('[payments] Lemon Squeezy subscriptions have no description field.');
277
+ }
278
+ const variantId = input.metadata?.variantId;
279
+ if (variantId === undefined) {
280
+ throw new Error('[payments] Lemon Squeezy only accepts a plan swap here. Pass ' +
281
+ '`metadata.variantId` with the variant to move the subscription to.');
282
+ }
283
+ const document = await this.#request(`/subscriptions/${encodeURIComponent(subscriptionGatewayId)}`, {
284
+ method: 'PATCH',
285
+ body: {
286
+ data: {
287
+ type: 'subscriptions',
288
+ id: String(subscriptionGatewayId),
289
+ attributes: {
290
+ variant_id: Number(variantId),
291
+ ...(input.metadata?.invoiceImmediately !== undefined
292
+ ? { invoice_immediately: Boolean(input.metadata.invoiceImmediately) }
293
+ : {}),
294
+ ...(input.metadata?.disableProrations !== undefined
295
+ ? { disable_prorations: Boolean(input.metadata.disableProrations) }
296
+ : {}),
297
+ },
298
+ },
299
+ },
300
+ });
301
+ return this.#mapSubscription(document.data);
302
+ }
303
+ async findSubscription(gatewayId) {
304
+ try {
305
+ const document = await this.#request(`/subscriptions/${encodeURIComponent(gatewayId)}`);
306
+ return this.#mapSubscription(document.data);
307
+ }
308
+ catch (error) {
309
+ if (isNotFound(error))
310
+ return null;
311
+ throw error;
312
+ }
313
+ }
314
+ // ── Invoices ─────────────────────────────────────────────────────────────────────────
315
+ /**
316
+ * Lists the customer's **orders** — the receipts Lemon Squeezy issues for a purchase.
317
+ * `GET /v1/orders` and `GET /v1/subscription-invoices` have no customer filter, so this
318
+ * walks the customer's own `orders` relationship instead. Renewal charges live under
319
+ * `/v1/subscription-invoices?filter[subscription_id]=…` and are not listed here.
320
+ */
321
+ async listInvoices(customerId) {
322
+ const collection = await this.#request(`/customers/${encodeURIComponent(customerId)}/orders`);
323
+ return collection.data.map((resource) => {
324
+ const order = resource.attributes;
325
+ const statusMap = {
326
+ paid: 'paid',
327
+ pending: 'open',
328
+ failed: 'uncollectible',
329
+ refunded: 'void',
330
+ };
331
+ return {
332
+ id: resource.id,
333
+ gatewayId: resource.id,
334
+ provider: this.provider,
335
+ ...(order.customer_id !== undefined ? { customerId: String(order.customer_id) } : {}),
336
+ status: statusMap[order.status] ?? 'open',
337
+ amount: { amount: order.total, currency: order.currency.toLowerCase() },
338
+ createdAt: order.created_at ?? new Date().toISOString(),
339
+ ...(order.urls?.receipt !== undefined ? { hostedPdfUrl: order.urls.receipt } : {}),
340
+ ...(order.order_number !== undefined ? { number: String(order.order_number) } : {}),
341
+ payload: order,
342
+ };
343
+ });
344
+ }
345
+ // ── Webhooks ─────────────────────────────────────────────────────────────────────────
346
+ /**
347
+ * Whether a delivery to `POST /payments/webhook/:provider` can be authenticated.
348
+ *
349
+ * `parseWebhook` already refuses without the secret — declaring it moves the refusal to boot.
350
+ */
351
+ get webhookVerification() {
352
+ return this.#webhookSecret !== undefined ? 'configured' : 'unconfigured';
353
+ }
354
+ parseWebhook(rawBody, headers) {
355
+ if (this.#webhookSecret === undefined) {
356
+ throw new Error('[payments] Lemon Squeezy webhook processing requires the signing secret set on ' +
357
+ 'the webhook in the dashboard. Set `LEMONSQUEEZY_WEBHOOK_SECRET` or pass ' +
358
+ '`webhookSecret` to `payments.lemonsqueezy()`.');
359
+ }
360
+ const signature = headerValue(headers, 'x-signature');
361
+ if (signature === undefined || signature === '') {
362
+ throw new Error('[payments] Missing `X-Signature` header on Lemon Squeezy webhook request.');
363
+ }
364
+ // HMAC-SHA256 over the raw body, sent as **hex**. The shared `verifyHmacSignature`
365
+ // helper digests to base64, so it does not fit here.
366
+ const expected = createHmac('sha256', this.#webhookSecret)
367
+ .update(rawBody, 'utf8')
368
+ .digest('hex');
369
+ if (!safeCompare(signature, expected)) {
370
+ throw new Error('[payments] Invalid Lemon Squeezy webhook signature.');
371
+ }
372
+ const payload = JSON.parse(rawBody);
373
+ const eventName = payload.meta?.event_name ?? 'unknown';
374
+ return {
375
+ id: this.#webhookEventId(eventName, payload),
376
+ provider: this.provider,
377
+ type: this.#mapWebhookType(eventName, payload),
378
+ ...(typeof payload.data?.attributes?.created_at === 'string'
379
+ ? { createdAt: payload.data.attributes.created_at }
380
+ : {}),
381
+ data: this.#mapWebhookData(eventName, payload),
382
+ raw: payload,
383
+ };
384
+ }
385
+ // ── Mappers ──────────────────────────────────────────────────────────────────────────
386
+ #mapCustomer(resource) {
387
+ const attributes = resource.attributes;
388
+ return {
389
+ id: resource.id,
390
+ ...(attributes.email !== undefined ? { email: attributes.email } : {}),
391
+ ...(attributes.name !== undefined ? { name: attributes.name } : {}),
392
+ };
393
+ }
394
+ #mapPayment(resource) {
395
+ const order = resource.attributes;
396
+ const statusMap = {
397
+ paid: 'paid',
398
+ pending: 'pending',
399
+ failed: 'failed',
400
+ refunded: 'refunded',
401
+ };
402
+ const result = {
403
+ id: resource.id,
404
+ gatewayId: resource.id,
405
+ provider: this.provider,
406
+ // Already an integer of the smallest unit — no `fromDecimal` here.
407
+ amount: { amount: order.total, currency: order.currency.toLowerCase() },
408
+ status: order.refunded === true ? 'refunded' : (statusMap[order.status] ?? 'pending'),
409
+ payload: order,
410
+ createdAt: order.created_at ?? new Date().toISOString(),
411
+ };
412
+ if (order.customer_id !== undefined)
413
+ result.customerId = String(order.customer_id);
414
+ if (order.urls?.receipt !== undefined)
415
+ result.hostedUrl = order.urls.receipt;
416
+ // Lemon Squeezy has no `paid_at`: an order only exists once payment succeeded, so
417
+ // `created_at` is the moment it was paid.
418
+ if (order.status === 'paid' && order.created_at !== undefined)
419
+ result.paidAt = order.created_at;
420
+ return result;
421
+ }
422
+ #mapSubscription(resource) {
423
+ const attributes = resource.attributes;
424
+ const statusMap = {
425
+ on_trial: 'trialing',
426
+ active: 'active',
427
+ past_due: 'past_due',
428
+ unpaid: 'past_due',
429
+ cancelled: 'canceled',
430
+ expired: 'ended',
431
+ // A paused Lemon Squeezy subscription still exists and will resume — but it is not
432
+ // billing now, so reporting it as `active` entitled a subscriber who is not paying.
433
+ paused: 'paused',
434
+ };
435
+ return {
436
+ id: resource.id,
437
+ gatewayId: resource.id,
438
+ provider: this.provider,
439
+ customerId: attributes.customer_id !== undefined ? String(attributes.customer_id) : '',
440
+ status: statusMap[attributes.status] ?? 'active',
441
+ planId: attributes.variant_id !== undefined ? String(attributes.variant_id) : '',
442
+ // No `amount`: a Lemon Squeezy subscription object carries no price at all — the
443
+ // price belongs to the variant, and the charged total only appears on each
444
+ // subscription invoice.
445
+ ...(attributes.trial_ends_at ? { trialEndsAt: attributes.trial_ends_at } : {}),
446
+ ...(attributes.ends_at ? { endsAt: attributes.ends_at } : {}),
447
+ // `renews_at` is the next collection date, i.e. the end of the current period.
448
+ ...(attributes.renews_at ? { currentPeriodEnd: attributes.renews_at } : {}),
449
+ payload: attributes,
450
+ createdAt: attributes.created_at ?? new Date().toISOString(),
451
+ };
452
+ }
453
+ #mapWebhookType(eventName, payload) {
454
+ switch (eventName) {
455
+ case 'order_created': {
456
+ // `order_created` fires for every order Lemon Squeezy records, including ones
457
+ // that failed or were refunded before delivery — so the order's own status, not
458
+ // the event name, decides what this means.
459
+ const status = payload.data?.attributes?.status;
460
+ if (status === 'refunded')
461
+ return 'payment.refunded';
462
+ if (status === 'failed')
463
+ return 'payment.failed';
464
+ if (status === 'paid')
465
+ return 'payment.succeeded';
466
+ return 'payment.updated';
467
+ }
468
+ case 'order_refunded':
469
+ case 'subscription_payment_refunded':
470
+ return 'payment.refunded';
471
+ case 'subscription_payment_success':
472
+ case 'subscription_payment_recovered':
473
+ return 'payment.succeeded';
474
+ case 'subscription_payment_failed':
475
+ return 'payment.failed';
476
+ case 'subscription_created':
477
+ return 'subscription.created';
478
+ case 'subscription_updated':
479
+ case 'subscription_plan_changed':
480
+ case 'subscription_paused':
481
+ case 'subscription_unpaused':
482
+ case 'subscription_resumed':
483
+ return 'subscription.updated';
484
+ case 'subscription_cancelled':
485
+ case 'subscription_expired':
486
+ return 'subscription.canceled';
487
+ default:
488
+ // `license_key_*`, `affiliate_activated`, `customer_updated` — passed through so an
489
+ // app handler can subscribe by the Lemon Squeezy name.
490
+ //
491
+ // There is deliberately no dispute type here: **Lemon Squeezy has NO dispute or
492
+ // chargeback event at all** — no warning, no chargeback, no resolution. The whole
493
+ // catalogue is `order_created`, `order_refunded`, the `subscription_*` family and
494
+ // `license_key_created` / `license_key_updated`; there is not even an
495
+ // `order_updated`, so an order that Lemon Squeezy later marks `fraudulent` (its
496
+ // status for a charged-back order) reaches this driver through no event whatsoever.
497
+ //
498
+ // That is the merchant-of-record bargain rather than an oversight: Lemon Squeezy is
499
+ // the seller on the buyer's statement, the chargeback is raised against Lemon
500
+ // Squeezy, and Lemon Squeezy "typically manages these disputes on behalf of the
501
+ // seller", deducting the amount plus a $15 dispute fee from the next payout. There
502
+ // is no deadline that belongs to you and no evidence you can file. Forcing an
503
+ // unrelated event into `payment.disputed` would invent a notification that does not
504
+ // exist; the honest answer is that the first you hear of a chargeback is the payout.
505
+ return eventName;
506
+ }
507
+ }
508
+ #mapWebhookData(eventName, payload) {
509
+ const resource = payload.data;
510
+ const externalReference = this.#readExternalReference(payload.meta?.custom_data);
511
+ const common = {
512
+ ...(externalReference !== undefined ? { externalReference } : {}),
513
+ ...(payload.meta?.test_mode !== undefined ? { testMode: payload.meta.test_mode } : {}),
514
+ };
515
+ if (resource?.type === 'subscriptions') {
516
+ const subscription = this.#mapSubscription(resource);
517
+ return {
518
+ gatewayId: subscription.gatewayId,
519
+ customerId: subscription.customerId,
520
+ status: subscription.status,
521
+ planId: subscription.planId,
522
+ ...(subscription.endsAt !== undefined ? { endsAt: subscription.endsAt } : {}),
523
+ ...common,
524
+ };
525
+ }
526
+ if (resource?.type === 'subscription-invoices') {
527
+ const invoice = resource.attributes;
528
+ return {
529
+ gatewayId: resource.id,
530
+ amount: invoice.total,
531
+ currency: invoice.currency.toLowerCase(),
532
+ ...(invoice.customer_id !== undefined ? { customerId: String(invoice.customer_id) } : {}),
533
+ ...(invoice.subscription_id !== undefined
534
+ ? { subscriptionId: String(invoice.subscription_id) }
535
+ : {}),
536
+ ...common,
537
+ };
538
+ }
539
+ if (resource?.type === 'orders') {
540
+ const payment = this.#mapPayment(resource);
541
+ return {
542
+ gatewayId: payment.gatewayId,
543
+ amount: payment.amount.amount,
544
+ currency: payment.amount.currency,
545
+ ...(payment.customerId !== undefined ? { customerId: payment.customerId } : {}),
546
+ ...common,
547
+ };
548
+ }
549
+ return { gatewayId: resource?.id ?? '', eventName, ...common };
550
+ }
551
+ // ── Helpers ──────────────────────────────────────────────────────────────────────────
552
+ /**
553
+ * Lemon Squeezy webhooks carry no event id, so one is derived. `updated_at` is part of
554
+ * it because two `subscription_updated` events for the same subscription are distinct
555
+ * events, and an id without it would make the second look like a replay to the billing
556
+ * layer's idempotency ledger.
557
+ */
558
+ #webhookEventId(eventName, payload) {
559
+ const updatedAt = payload.data?.attributes?.updated_at;
560
+ const parts = [eventName, payload.data?.type ?? 'unknown', payload.data?.id ?? 'unknown'];
561
+ if (typeof updatedAt === 'string')
562
+ parts.push(updatedAt);
563
+ return parts.join(':');
564
+ }
565
+ /**
566
+ * The routing key is written to `checkout_data.custom.external_reference`, which Lemon
567
+ * Squeezy echoes on every webhook for the resulting order and subscription as
568
+ * `meta.custom_data`. `metadata.externalReference` is still honoured as a fallback for
569
+ * callers written before `CheckoutInput.externalReference` existed.
570
+ */
571
+ #customData(input) {
572
+ const { email: _email, name: _name, taxNumber: _taxNumber, city: _city, region: _region, country: _country, externalReference: fromMetadata, ...rest } = input.metadata ?? {};
573
+ const reference = input.externalReference ?? (typeof fromMetadata === 'string' ? fromMetadata : undefined);
574
+ const custom = {
575
+ ...rest,
576
+ ...(reference !== undefined ? { external_reference: reference } : {}),
577
+ };
578
+ return Object.keys(custom).length > 0 ? custom : undefined;
579
+ }
580
+ #readExternalReference(customData) {
581
+ const value = customData?.external_reference;
582
+ // Lemon Squeezy returns custom values as strings, but a number written at checkout
583
+ // has been seen coming back as one — normalize rather than silently dropping the key.
584
+ if (typeof value === 'string')
585
+ return value;
586
+ if (typeof value === 'number')
587
+ return String(value);
588
+ return undefined;
589
+ }
590
+ /**
591
+ * Lemon Squeezy has **no request deduplication** — no `Idempotency-Key` header, no
592
+ * request-id field on any endpoint. So a key handed to this driver is refused rather
593
+ * than accepted and dropped: silently dropping it turns a caller's retry guarantee into
594
+ * a second refund or a second checkout.
595
+ */
596
+ #refuseIdempotencyKey(key, operation) {
597
+ if (key === undefined)
598
+ return;
599
+ throw new Error(`[payments] Lemon Squeezy has no idempotency mechanism, so \`idempotencyKey\` cannot be honoured on ${operation}(). Its API deduplicates nothing, and a retried request performs the operation a second time — deduplicate on your side (persist the key and check it) before calling.`);
600
+ }
601
+ #refuseTaxId(taxId) {
602
+ if (taxId === undefined)
603
+ return;
604
+ throw new Error('[payments] Lemon Squeezy has no tax id on a customer — the buyer enters a tax ' +
605
+ 'number at checkout. Pass it as `metadata.taxNumber` on `createCheckout()`.');
606
+ }
607
+ /**
608
+ * JSON:API needs `Accept` and `Content-Type` of `application/vnd.api+json`, and the
609
+ * shared `httpRequest` helper hardcodes `application/json` with no way to add headers —
610
+ * so this driver does its own `fetch`, throwing the same `Error` with a `status`
611
+ * property so `isNotFound` keeps working.
612
+ */
613
+ async #request(path, options = {}) {
614
+ const requestInit = {
615
+ method: options.method ?? 'GET',
616
+ headers: {
617
+ Accept: 'application/vnd.api+json',
618
+ 'Content-Type': 'application/vnd.api+json',
619
+ Authorization: `Bearer ${this.#apiKey}`,
620
+ },
621
+ };
622
+ if (options.body !== undefined)
623
+ requestInit.body = JSON.stringify(options.body);
624
+ const response = await fetch(`${this.#baseUrl}${path}`, requestInit);
625
+ if (!response.ok) {
626
+ const text = await response.text();
627
+ throw Object.assign(new Error(`[payments] HTTP request failed (${response.status}): ${text}`), { status: response.status });
628
+ }
629
+ return (await response.json());
630
+ }
631
+ }
632
+ //# sourceMappingURL=lemonsqueezy.js.map