@adonis-agora/payments 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (319) hide show
  1. package/CHANGELOG.md +2347 -0
  2. package/dist/assets/spa/assets/index-CT5Zon6P.css +1 -0
  3. package/dist/assets/spa/assets/index-CvEbJEnq.js +41 -0
  4. package/dist/assets/spa/assets/index-CvEbJEnq.js.map +1 -0
  5. package/dist/assets/spa/index.html +20 -0
  6. package/dist/commands/main.d.ts +4 -3
  7. package/dist/commands/main.d.ts.map +1 -1
  8. package/dist/commands/main.js +9 -2
  9. package/dist/commands/main.js.map +1 -1
  10. package/dist/commands/payments_health.d.ts +31 -0
  11. package/dist/commands/payments_health.d.ts.map +1 -0
  12. package/dist/commands/payments_health.js +113 -0
  13. package/dist/commands/payments_health.js.map +1 -0
  14. package/dist/commands/payments_sync.d.ts +18 -4
  15. package/dist/commands/payments_sync.d.ts.map +1 -1
  16. package/dist/commands/payments_sync.js +136 -40
  17. package/dist/commands/payments_sync.js.map +1 -1
  18. package/dist/configure.d.ts +12 -5
  19. package/dist/configure.d.ts.map +1 -1
  20. package/dist/configure.js +18 -5
  21. package/dist/configure.js.map +1 -1
  22. package/dist/providers/dashboard_provider.d.ts +97 -0
  23. package/dist/providers/dashboard_provider.d.ts.map +1 -0
  24. package/dist/providers/dashboard_provider.js +430 -0
  25. package/dist/providers/dashboard_provider.js.map +1 -0
  26. package/dist/providers/payments_client_provider.d.ts +41 -0
  27. package/dist/providers/payments_client_provider.d.ts.map +1 -0
  28. package/dist/providers/payments_client_provider.js +95 -0
  29. package/dist/providers/payments_client_provider.js.map +1 -0
  30. package/dist/providers/payments_provider.d.ts +35 -0
  31. package/dist/providers/payments_provider.d.ts.map +1 -1
  32. package/dist/providers/payments_provider.js +250 -36
  33. package/dist/providers/payments_provider.js.map +1 -1
  34. package/dist/src/billing/billing_health.d.ts +85 -0
  35. package/dist/src/billing/billing_health.d.ts.map +1 -0
  36. package/dist/src/billing/billing_health.js +155 -0
  37. package/dist/src/billing/billing_health.js.map +1 -0
  38. package/dist/src/billing/billing_overview.d.ts +4 -3
  39. package/dist/src/billing/billing_overview.d.ts.map +1 -1
  40. package/dist/src/billing/billing_overview.js +14 -5
  41. package/dist/src/billing/billing_overview.js.map +1 -1
  42. package/dist/src/billing/billing_store.d.ts +634 -5
  43. package/dist/src/billing/billing_store.d.ts.map +1 -1
  44. package/dist/src/billing/billing_store.js +18 -1
  45. package/dist/src/billing/billing_store.js.map +1 -1
  46. package/dist/src/billing/index.d.ts +2 -1
  47. package/dist/src/billing/index.d.ts.map +1 -1
  48. package/dist/src/billing/index.js +1 -0
  49. package/dist/src/billing/index.js.map +1 -1
  50. package/dist/src/billing/list_query.d.ts +16 -0
  51. package/dist/src/billing/list_query.d.ts.map +1 -0
  52. package/dist/src/billing/list_query.js +28 -0
  53. package/dist/src/billing/list_query.js.map +1 -0
  54. package/dist/src/billing/lucid_billing_store.d.ts +122 -4
  55. package/dist/src/billing/lucid_billing_store.d.ts.map +1 -1
  56. package/dist/src/billing/lucid_billing_store.js +811 -13
  57. package/dist/src/billing/lucid_billing_store.js.map +1 -1
  58. package/dist/src/billing/mixins/index.d.ts +3 -0
  59. package/dist/src/billing/mixins/index.d.ts.map +1 -1
  60. package/dist/src/billing/mixins/index.js +3 -0
  61. package/dist/src/billing/mixins/index.js.map +1 -1
  62. package/dist/src/billing/mixins/with_audit_event.d.ts +48 -0
  63. package/dist/src/billing/mixins/with_audit_event.d.ts.map +1 -0
  64. package/dist/src/billing/mixins/with_audit_event.js +77 -0
  65. package/dist/src/billing/mixins/with_audit_event.js.map +1 -0
  66. package/dist/src/billing/mixins/with_customer.d.ts +39 -0
  67. package/dist/src/billing/mixins/with_customer.d.ts.map +1 -0
  68. package/dist/src/billing/mixins/with_customer.js +68 -0
  69. package/dist/src/billing/mixins/with_customer.js.map +1 -0
  70. package/dist/src/billing/mixins/with_dispute.d.ts +68 -0
  71. package/dist/src/billing/mixins/with_dispute.d.ts.map +1 -0
  72. package/dist/src/billing/mixins/with_dispute.js +83 -0
  73. package/dist/src/billing/mixins/with_dispute.js.map +1 -0
  74. package/dist/src/billing/mixins/with_payment.d.ts +42 -0
  75. package/dist/src/billing/mixins/with_payment.d.ts.map +1 -1
  76. package/dist/src/billing/mixins/with_payment.js +29 -3
  77. package/dist/src/billing/mixins/with_payment.js.map +1 -1
  78. package/dist/src/billing/mixins/with_subscription.d.ts +7 -0
  79. package/dist/src/billing/mixins/with_subscription.d.ts.map +1 -1
  80. package/dist/src/billing/mixins/with_subscription.js +15 -1
  81. package/dist/src/billing/mixins/with_subscription.js.map +1 -1
  82. package/dist/src/billing/mixins/with_usage_event.d.ts +7 -0
  83. package/dist/src/billing/mixins/with_usage_event.d.ts.map +1 -1
  84. package/dist/src/billing/mixins/with_usage_event.js +15 -1
  85. package/dist/src/billing/mixins/with_usage_event.js.map +1 -1
  86. package/dist/src/billing/mixins/with_webhook_event.d.ts +18 -0
  87. package/dist/src/billing/mixins/with_webhook_event.d.ts.map +1 -1
  88. package/dist/src/billing/mixins/with_webhook_event.js +18 -1
  89. package/dist/src/billing/mixins/with_webhook_event.js.map +1 -1
  90. package/dist/src/billing/resolve_dispatch.d.ts +22 -0
  91. package/dist/src/billing/resolve_dispatch.d.ts.map +1 -0
  92. package/dist/src/billing/resolve_dispatch.js +44 -0
  93. package/dist/src/billing/resolve_dispatch.js.map +1 -0
  94. package/dist/src/billing/resolve_store.d.ts +18 -0
  95. package/dist/src/billing/resolve_store.d.ts.map +1 -0
  96. package/dist/src/billing/resolve_store.js +35 -0
  97. package/dist/src/billing/resolve_store.js.map +1 -0
  98. package/dist/src/billing/schema.d.ts +108 -0
  99. package/dist/src/billing/schema.d.ts.map +1 -0
  100. package/dist/src/billing/schema.js +350 -0
  101. package/dist/src/billing/schema.js.map +1 -0
  102. package/dist/src/billing/webhook_dispatcher.d.ts +95 -16
  103. package/dist/src/billing/webhook_dispatcher.d.ts.map +1 -1
  104. package/dist/src/billing/webhook_dispatcher.js +186 -33
  105. package/dist/src/billing/webhook_dispatcher.js.map +1 -1
  106. package/dist/src/billing/webhook_events.d.ts +67 -1
  107. package/dist/src/billing/webhook_events.d.ts.map +1 -1
  108. package/dist/src/billing/webhook_events.js +15 -0
  109. package/dist/src/billing/webhook_events.js.map +1 -1
  110. package/dist/src/billing/webhook_processor.d.ts.map +1 -1
  111. package/dist/src/billing/webhook_processor.js +325 -5
  112. package/dist/src/billing/webhook_processor.js.map +1 -1
  113. package/dist/src/client/define_config.d.ts +150 -0
  114. package/dist/src/client/define_config.d.ts.map +1 -0
  115. package/dist/src/client/define_config.js +98 -0
  116. package/dist/src/client/define_config.js.map +1 -0
  117. package/dist/src/client/handlers.d.ts +71 -0
  118. package/dist/src/client/handlers.d.ts.map +1 -0
  119. package/dist/src/client/handlers.js +119 -0
  120. package/dist/src/client/handlers.js.map +1 -0
  121. package/dist/src/client/index.d.ts +16 -0
  122. package/dist/src/client/index.d.ts.map +1 -0
  123. package/dist/src/client/index.js +13 -0
  124. package/dist/src/client/index.js.map +1 -0
  125. package/dist/src/client/statuses.d.ts +38 -0
  126. package/dist/src/client/statuses.d.ts.map +1 -0
  127. package/dist/src/client/statuses.js +42 -0
  128. package/dist/src/client/statuses.js.map +1 -0
  129. package/dist/src/dashboard/actions.d.ts +143 -0
  130. package/dist/src/dashboard/actions.d.ts.map +1 -0
  131. package/dist/src/dashboard/actions.js +112 -0
  132. package/dist/src/dashboard/actions.js.map +1 -0
  133. package/dist/src/dashboard/auth.d.ts +136 -0
  134. package/dist/src/dashboard/auth.d.ts.map +1 -0
  135. package/dist/src/dashboard/auth.js +152 -0
  136. package/dist/src/dashboard/auth.js.map +1 -0
  137. package/dist/src/dashboard/define_config.d.ts +80 -0
  138. package/dist/src/dashboard/define_config.d.ts.map +1 -0
  139. package/dist/src/dashboard/define_config.js +83 -0
  140. package/dist/src/dashboard/define_config.js.map +1 -0
  141. package/dist/src/dashboard/handlers.d.ts +281 -0
  142. package/dist/src/dashboard/handlers.d.ts.map +1 -0
  143. package/dist/src/dashboard/handlers.js +960 -0
  144. package/dist/src/dashboard/handlers.js.map +1 -0
  145. package/dist/src/dashboard/index.d.ts +18 -0
  146. package/dist/src/dashboard/index.d.ts.map +1 -0
  147. package/dist/src/dashboard/index.js +14 -0
  148. package/dist/src/dashboard/index.js.map +1 -0
  149. package/dist/src/dashboard/login_page.d.ts +23 -0
  150. package/dist/src/dashboard/login_page.d.ts.map +1 -0
  151. package/dist/src/dashboard/login_page.js +170 -0
  152. package/dist/src/dashboard/login_page.js.map +1 -0
  153. package/dist/src/dashboard/period.d.ts +31 -0
  154. package/dist/src/dashboard/period.d.ts.map +1 -0
  155. package/dist/src/dashboard/period.js +44 -0
  156. package/dist/src/dashboard/period.js.map +1 -0
  157. package/dist/src/dashboard/session_cookie.d.ts +42 -0
  158. package/dist/src/dashboard/session_cookie.d.ts.map +1 -0
  159. package/dist/src/dashboard/session_cookie.js +97 -0
  160. package/dist/src/dashboard/session_cookie.js.map +1 -0
  161. package/dist/src/dashboard/spa.d.ts +24 -0
  162. package/dist/src/dashboard/spa.d.ts.map +1 -0
  163. package/dist/src/dashboard/spa.js +52 -0
  164. package/dist/src/dashboard/spa.js.map +1 -0
  165. package/dist/src/define_config.d.ts +244 -9
  166. package/dist/src/define_config.d.ts.map +1 -1
  167. package/dist/src/define_config.js +97 -1
  168. package/dist/src/define_config.js.map +1 -1
  169. package/dist/src/diagnostics.d.ts +287 -7
  170. package/dist/src/diagnostics.d.ts.map +1 -1
  171. package/dist/src/diagnostics.js +317 -1
  172. package/dist/src/diagnostics.js.map +1 -1
  173. package/dist/src/driver.d.ts +139 -5
  174. package/dist/src/driver.d.ts.map +1 -1
  175. package/dist/src/drivers/abacate.d.ts +12 -2
  176. package/dist/src/drivers/abacate.d.ts.map +1 -1
  177. package/dist/src/drivers/abacate.js +112 -19
  178. package/dist/src/drivers/abacate.js.map +1 -1
  179. package/dist/src/drivers/adyen.d.ts +164 -0
  180. package/dist/src/drivers/adyen.d.ts.map +1 -0
  181. package/dist/src/drivers/adyen.js +678 -0
  182. package/dist/src/drivers/adyen.js.map +1 -0
  183. package/dist/src/drivers/asaas.d.ts +52 -2
  184. package/dist/src/drivers/asaas.d.ts.map +1 -1
  185. package/dist/src/drivers/asaas.js +364 -29
  186. package/dist/src/drivers/asaas.js.map +1 -1
  187. package/dist/src/drivers/dodo.d.ts +135 -0
  188. package/dist/src/drivers/dodo.d.ts.map +1 -0
  189. package/dist/src/drivers/dodo.js +864 -0
  190. package/dist/src/drivers/dodo.js.map +1 -0
  191. package/dist/src/drivers/efi.d.ts +98 -0
  192. package/dist/src/drivers/efi.d.ts.map +1 -0
  193. package/dist/src/drivers/efi.js +628 -0
  194. package/dist/src/drivers/efi.js.map +1 -0
  195. package/dist/src/drivers/infinitepay.d.ts +156 -0
  196. package/dist/src/drivers/infinitepay.d.ts.map +1 -0
  197. package/dist/src/drivers/infinitepay.js +321 -0
  198. package/dist/src/drivers/infinitepay.js.map +1 -0
  199. package/dist/src/drivers/lemonsqueezy.d.ts +125 -0
  200. package/dist/src/drivers/lemonsqueezy.d.ts.map +1 -0
  201. package/dist/src/drivers/lemonsqueezy.js +632 -0
  202. package/dist/src/drivers/lemonsqueezy.js.map +1 -0
  203. package/dist/src/drivers/mercadopago.d.ts +97 -0
  204. package/dist/src/drivers/mercadopago.d.ts.map +1 -0
  205. package/dist/src/drivers/mercadopago.js +844 -0
  206. package/dist/src/drivers/mercadopago.js.map +1 -0
  207. package/dist/src/drivers/mollie.d.ts +125 -0
  208. package/dist/src/drivers/mollie.d.ts.map +1 -0
  209. package/dist/src/drivers/mollie.js +812 -0
  210. package/dist/src/drivers/mollie.js.map +1 -0
  211. package/dist/src/drivers/paddle.d.ts +148 -0
  212. package/dist/src/drivers/paddle.d.ts.map +1 -0
  213. package/dist/src/drivers/paddle.js +738 -0
  214. package/dist/src/drivers/paddle.js.map +1 -0
  215. package/dist/src/drivers/pagarme.d.ts +97 -0
  216. package/dist/src/drivers/pagarme.d.ts.map +1 -0
  217. package/dist/src/drivers/pagarme.js +829 -0
  218. package/dist/src/drivers/pagarme.js.map +1 -0
  219. package/dist/src/drivers/pagbank.d.ts +57 -0
  220. package/dist/src/drivers/pagbank.d.ts.map +1 -0
  221. package/dist/src/drivers/pagbank.js +499 -0
  222. package/dist/src/drivers/pagbank.js.map +1 -0
  223. package/dist/src/drivers/paypal.d.ts +120 -0
  224. package/dist/src/drivers/paypal.d.ts.map +1 -0
  225. package/dist/src/drivers/paypal.js +891 -0
  226. package/dist/src/drivers/paypal.js.map +1 -0
  227. package/dist/src/drivers/polar.d.ts +118 -0
  228. package/dist/src/drivers/polar.d.ts.map +1 -0
  229. package/dist/src/drivers/polar.js +604 -0
  230. package/dist/src/drivers/polar.js.map +1 -0
  231. package/dist/src/drivers/razorpay.d.ts +103 -0
  232. package/dist/src/drivers/razorpay.d.ts.map +1 -0
  233. package/dist/src/drivers/razorpay.js +867 -0
  234. package/dist/src/drivers/razorpay.js.map +1 -0
  235. package/dist/src/drivers/shared.d.ts +33 -0
  236. package/dist/src/drivers/shared.d.ts.map +1 -0
  237. package/dist/src/drivers/shared.js +37 -0
  238. package/dist/src/drivers/shared.js.map +1 -0
  239. package/dist/src/drivers/square.d.ts +133 -0
  240. package/dist/src/drivers/square.d.ts.map +1 -0
  241. package/dist/src/drivers/square.js +905 -0
  242. package/dist/src/drivers/square.js.map +1 -0
  243. package/dist/src/drivers/stripe.d.ts +40 -3
  244. package/dist/src/drivers/stripe.d.ts.map +1 -1
  245. package/dist/src/drivers/stripe.js +806 -25
  246. package/dist/src/drivers/stripe.js.map +1 -1
  247. package/dist/src/drivers/woovi.d.ts +10 -3
  248. package/dist/src/drivers/woovi.d.ts.map +1 -1
  249. package/dist/src/drivers/woovi.js +162 -29
  250. package/dist/src/drivers/woovi.js.map +1 -1
  251. package/dist/src/ensure_customer.d.ts +34 -1
  252. package/dist/src/ensure_customer.d.ts.map +1 -1
  253. package/dist/src/ensure_customer.js +29 -4
  254. package/dist/src/ensure_customer.js.map +1 -1
  255. package/dist/src/http.d.ts +37 -3
  256. package/dist/src/http.d.ts.map +1 -1
  257. package/dist/src/http.js +69 -5
  258. package/dist/src/http.js.map +1 -1
  259. package/dist/src/index.d.ts +40 -10
  260. package/dist/src/index.d.ts.map +1 -1
  261. package/dist/src/index.js +22 -4
  262. package/dist/src/index.js.map +1 -1
  263. package/dist/src/invoice/emit_invoice.d.ts.map +1 -1
  264. package/dist/src/invoice/emit_invoice.js +31 -11
  265. package/dist/src/invoice/emit_invoice.js.map +1 -1
  266. package/dist/src/money.d.ts +23 -5
  267. package/dist/src/money.d.ts.map +1 -1
  268. package/dist/src/money.js +72 -7
  269. package/dist/src/money.js.map +1 -1
  270. package/dist/src/payments_manager.d.ts +8 -1
  271. package/dist/src/payments_manager.d.ts.map +1 -1
  272. package/dist/src/payments_manager.js +168 -6
  273. package/dist/src/payments_manager.js.map +1 -1
  274. package/dist/src/services/main.d.ts +38 -0
  275. package/dist/src/services/main.d.ts.map +1 -1
  276. package/dist/src/services/main.js +130 -0
  277. package/dist/src/services/main.js.map +1 -1
  278. package/dist/src/telescope/index.d.ts +2 -0
  279. package/dist/src/telescope/index.d.ts.map +1 -1
  280. package/dist/src/telescope/index.js +1 -0
  281. package/dist/src/telescope/index.js.map +1 -1
  282. package/dist/src/telescope/payments_watcher.d.ts +10 -4
  283. package/dist/src/telescope/payments_watcher.d.ts.map +1 -1
  284. package/dist/src/telescope/payments_watcher.js +65 -13
  285. package/dist/src/telescope/payments_watcher.js.map +1 -1
  286. package/dist/src/telescope/register.d.ts +62 -0
  287. package/dist/src/telescope/register.d.ts.map +1 -0
  288. package/dist/src/telescope/register.js +48 -0
  289. package/dist/src/telescope/register.js.map +1 -0
  290. package/dist/src/testing/fake_payments.d.ts +62 -0
  291. package/dist/src/testing/fake_payments.d.ts.map +1 -0
  292. package/dist/src/testing/fake_payments.js +82 -0
  293. package/dist/src/testing/fake_payments.js.map +1 -0
  294. package/dist/src/testing/in_memory_billing_store.d.ts +151 -2
  295. package/dist/src/testing/in_memory_billing_store.d.ts.map +1 -1
  296. package/dist/src/testing/in_memory_billing_store.js +517 -4
  297. package/dist/src/testing/in_memory_billing_store.js.map +1 -1
  298. package/dist/src/testing/index.d.ts +2 -1
  299. package/dist/src/testing/index.d.ts.map +1 -1
  300. package/dist/src/testing/index.js +1 -0
  301. package/dist/src/testing/index.js.map +1 -1
  302. package/dist/src/types.d.ts +161 -5
  303. package/dist/src/types.d.ts.map +1 -1
  304. package/dist/src/types.js +30 -1
  305. package/dist/src/types.js.map +1 -1
  306. package/dist/src/webhook_handlers.d.ts +96 -2
  307. package/dist/src/webhook_handlers.d.ts.map +1 -1
  308. package/dist/src/webhook_handlers.js +91 -10
  309. package/dist/src/webhook_handlers.js.map +1 -1
  310. package/dist/src/webhook_security.d.ts +94 -3
  311. package/dist/src/webhook_security.d.ts.map +1 -1
  312. package/dist/src/webhook_security.js +173 -9
  313. package/dist/src/webhook_security.js.map +1 -1
  314. package/dist/stubs/config/payments.stub +13 -0
  315. package/dist/stubs/config/payments_client.stub +42 -0
  316. package/dist/stubs/config/payments_dashboard.stub +38 -0
  317. package/dist/stubs/database/migrations/create_billing_tables.stub +35 -77
  318. package/dist/stubs/make/webhook_handler/main.stub +29 -11
  319. package/package.json +60 -25
@@ -0,0 +1,867 @@
1
+ import { publishPaymentDiagnostics, publishRefundDiagnostics, publishSubscriptionDiagnostics, } from '../diagnostics.js';
2
+ import { headerValue, httpRequest, isNotFound } from '../http.js';
3
+ import { emitInvoiceIfRequested } from '../invoice/emit_invoice.js';
4
+ import { verifyHmacSignature } from '../webhook_security.js';
5
+ import { requireCredential, requireCurrency } from './shared.js';
6
+ /** Razorpay stamps every id with its entity: `order_…`, `pay_…`, `plink_…`, `sub_…`. */
7
+ const ORDER_PREFIX = 'order_';
8
+ const PAYMENT_PREFIX = 'pay_';
9
+ const PAYMENT_LINK_PREFIX = 'plink_';
10
+ /** Razorpay rejects a `receipt` longer than this, so catch it before the round trip. */
11
+ const RECEIPT_MAX_LENGTH = 40;
12
+ /** `X-Refund-Idempotency` must be at least this long, or Razorpay rejects the refund. */
13
+ const REFUND_IDEMPOTENCY_MIN_LENGTH = 10;
14
+ /**
15
+ * Razorpay driver — the dominant Indian gateway (UPI, cards, netbanking, wallets) with
16
+ * native subscription billing. Plain REST over `fetch`, no SDK.
17
+ *
18
+ * Two shapes of this API are worth knowing before reading the mappings:
19
+ *
20
+ * **Orders, not charges.** Razorpay has no "charge this customer" endpoint for a normal
21
+ * integration: you create an *order* (an amount you expect to collect) and the payer
22
+ * settles it in Razorpay Checkout, which produces a *payment* against that order. So
23
+ * `charge()` creates an order and returns a `pending` payment whose `gatewayId` is the
24
+ * `order_…` id — the id you hand to Checkout. `findPayment` accepts either id and branches
25
+ * on the prefix.
26
+ *
27
+ * **Authorize then capture.** A Razorpay payment is `authorized` (funds held on the
28
+ * instrument) before it is `captured` (money moving to you). This package's
29
+ * `BillingStatus` has no name for that middle state, so `authorized` maps to `pending` —
30
+ * see {@link RazorpayDriver.capturePayment}.
31
+ *
32
+ * Test mode is chosen by the key pair (`rzp_test_…`), not by a different host, so there is
33
+ * no `sandbox` option: point the driver at test keys and it talks to the test account.
34
+ */
35
+ export class RazorpayDriver {
36
+ provider = 'razorpay';
37
+ /**
38
+ * A Razorpay order carries no payment method — the payer picks card, UPI, netbanking or
39
+ * a wallet inside Checkout — so `undefined` ("let the customer choose") is the only
40
+ * honest entry. Routing `methods: { credit_card: 'razorpay' }` therefore fails at the
41
+ * manager, and it should: nothing in the Orders API can promise a card was used. Read
42
+ * the method the payer actually chose off `payment.method` on the webhook.
43
+ */
44
+ supportedMethods = ['undefined'];
45
+ capabilities = { refunds: true, invoices: true, subscriptions: true };
46
+ #baseUrl = 'https://api.razorpay.com/v1';
47
+ #authHeader;
48
+ #currency;
49
+ #webhookSecret;
50
+ #invoiceCtx;
51
+ constructor(ctx, config) {
52
+ this.#invoiceCtx = ctx;
53
+ const keyId = requireCredential({
54
+ driver: 'razorpay',
55
+ option: 'keyId',
56
+ env: 'RAZORPAY_KEY_ID',
57
+ value: config.keyId,
58
+ });
59
+ const keySecret = requireCredential({
60
+ driver: 'razorpay',
61
+ option: 'keySecret',
62
+ env: 'RAZORPAY_KEY_SECRET',
63
+ value: config.keySecret,
64
+ });
65
+ this.#currency = requireCurrency('razorpay', config.currency);
66
+ this.#webhookSecret = config.webhookSecret ?? process.env.RAZORPAY_WEBHOOK_SECRET;
67
+ this.#authHeader = {
68
+ name: 'Authorization',
69
+ value: `Basic ${Buffer.from(`${keyId}:${keySecret}`, 'utf8').toString('base64')}`,
70
+ };
71
+ }
72
+ // ── Customers ────────────────────────────────────────────────────────────────────────
73
+ async createCustomer(input) {
74
+ this.#refuseIdempotencyKey(input.idempotencyKey, 'customer creation', 'POST /v1/customers');
75
+ const body = {
76
+ ...(input.name !== undefined ? { name: input.name } : {}),
77
+ ...(input.email !== undefined ? { email: input.email } : {}),
78
+ ...(typeof input.metadata?.contact === 'string' ? { contact: input.metadata.contact } : {}),
79
+ // GSTIN is the only tax id a Razorpay customer has a field for; a PAN or anything
80
+ // else the app calls `taxId` would be silently dropped, so it goes to notes instead.
81
+ ...(input.taxId !== undefined ? { notes: { tax_id: input.taxId } } : {}),
82
+ // Returning the existing customer is friendlier than a 400 on a retried signup, and
83
+ // matches how every other driver here treats create-customer.
84
+ fail_existing: '0',
85
+ };
86
+ const data = await this.#request('/customers', {
87
+ method: 'POST',
88
+ body,
89
+ });
90
+ return this.#mapCustomer(data);
91
+ }
92
+ async findCustomer(customerId) {
93
+ try {
94
+ const data = await this.#request(`/customers/${customerId}`);
95
+ return this.#mapCustomer(data);
96
+ }
97
+ catch (error) {
98
+ if (isNotFound(error))
99
+ return null;
100
+ throw error;
101
+ }
102
+ }
103
+ async updateCustomer(customerId, input) {
104
+ const body = {
105
+ ...(input.name !== undefined ? { name: input.name } : {}),
106
+ ...(input.email !== undefined ? { email: input.email } : {}),
107
+ ...(typeof input.metadata?.contact === 'string' ? { contact: input.metadata.contact } : {}),
108
+ };
109
+ const data = await this.#request(`/customers/${customerId}`, {
110
+ method: 'PUT',
111
+ body,
112
+ });
113
+ return this.#mapCustomer(data);
114
+ }
115
+ // ── Payments ─────────────────────────────────────────────────────────────────────────
116
+ async charge(input) {
117
+ if (input.card !== undefined || input.paymentMethodId !== undefined) {
118
+ throw new Error('[payments] Razorpay does not accept a card token server-side on a normal account: ' +
119
+ 'the payer authorizes the card inside Razorpay Checkout. Create the charge without ' +
120
+ '`card`/`paymentMethodId` and hand the returned `order_…` id to Checkout.');
121
+ }
122
+ if (input.method !== undefined && input.method !== 'undefined') {
123
+ throw new Error(`[payments] Razorpay cannot pin a payment method on an order: the payer picks card, UPI, netbanking or a wallet inside Checkout, so "${input.method}" is a promise the Orders API cannot keep. Drop \`method\` and read \`payment.method\` off the webhook.`);
124
+ }
125
+ if (input.split !== undefined && input.split.length > 0) {
126
+ throw new Error('[payments] Razorpay splits payments through Route transfers, which this driver does ' +
127
+ 'not implement. Create the transfers directly against the Razorpay Route API.');
128
+ }
129
+ if (input.idempotencyKey !== undefined && input.idempotencyKey.length > RECEIPT_MAX_LENGTH) {
130
+ throw new Error(`[payments] Razorpay carries \`idempotencyKey\` as the order's \`receipt\`, capped at ${RECEIPT_MAX_LENGTH} characters; got ${input.idempotencyKey.length}.`);
131
+ }
132
+ const notes = this.#notes(input.externalReference, {
133
+ ...(input.description !== undefined ? { description: input.description } : {}),
134
+ // Razorpay orders have no customer field at all, so the id is recorded as a note.
135
+ // It documents the intent; it does not link the two entities at the gateway.
136
+ ...(input.customerId !== undefined ? { customer_id: input.customerId } : {}),
137
+ ...input.metadata,
138
+ });
139
+ const body = {
140
+ // No conversion: Razorpay's `amount` is already the integer minor unit (paise for INR),
141
+ // the same unit as this package's `Money`. The BR drivers around this one divide by 100
142
+ // because their gateways want decimal reais; doing it here would bill a hundredth.
143
+ amount: input.amount,
144
+ currency: (input.currency ?? this.#currency).toUpperCase(),
145
+ // Razorpay has no idempotency header; `receipt` is the account-level uniqueness key —
146
+ // a second create with the same receipt is rejected instead of creating a twin order.
147
+ ...(input.idempotencyKey !== undefined ? { receipt: input.idempotencyKey } : {}),
148
+ ...(notes !== undefined ? { notes } : {}),
149
+ };
150
+ const data = await this.#request('/orders', { method: 'POST', body });
151
+ const payment = this.#mapOrder(data, input.customerId);
152
+ await emitInvoiceIfRequested(this.#invoiceCtx, input, payment, this);
153
+ publishPaymentDiagnostics(payment);
154
+ return payment;
155
+ }
156
+ async findPayment(gatewayId) {
157
+ try {
158
+ if (gatewayId.startsWith(ORDER_PREFIX)) {
159
+ return this.#mapOrder(await this.#request(`/orders/${gatewayId}`));
160
+ }
161
+ if (gatewayId.startsWith(PAYMENT_LINK_PREFIX)) {
162
+ return this.#mapPaymentLink(await this.#request(`/payment_links/${gatewayId}`));
163
+ }
164
+ return this.#mapPayment(await this.#request(`/payments/${gatewayId}`));
165
+ }
166
+ catch (error) {
167
+ if (isNotFound(error))
168
+ return null;
169
+ throw error;
170
+ }
171
+ }
172
+ async refund(paymentGatewayId, amount, options) {
173
+ if (!paymentGatewayId.startsWith(PAYMENT_PREFIX)) {
174
+ throw new Error(`[payments] Razorpay refunds a payment (\`pay_…\`), not "${paymentGatewayId}". An order or payment link is what the payer settles; read the resulting payment id off the \`payment.captured\` webhook (or \`GET /orders/:id/payments\`) and refund that.`);
175
+ }
176
+ const body = {
177
+ ...(amount !== undefined ? { amount } : {}),
178
+ };
179
+ const data = await this.#request(`/payments/${paymentGatewayId}/refund`, {
180
+ method: 'POST',
181
+ body,
182
+ // Refunds are the one Razorpay operation with a documented idempotency mechanism,
183
+ // and it is a header of its own name — not `Idempotency-Key`.
184
+ ...this.#refundIdempotency(options?.idempotencyKey),
185
+ });
186
+ const refund = {
187
+ id: data.id,
188
+ gatewayId: data.id,
189
+ provider: this.provider,
190
+ amount: { amount: data.amount, currency: data.currency.toLowerCase() },
191
+ // `pending` is the normal first answer here — Razorpay confirms the final state on
192
+ // the `refund.processed` webhook, which is the only thing that means money moved.
193
+ status: data.status === 'processed' ? 'succeeded' : data.status === 'failed' ? 'failed' : 'pending',
194
+ createdAt: this.#toIso(data.created_at) ?? new Date().toISOString(),
195
+ };
196
+ publishRefundDiagnostics(refund);
197
+ return refund;
198
+ }
199
+ /**
200
+ * Capture an authorized payment — the second half of Razorpay's authorize/capture split.
201
+ *
202
+ * Not part of {@link PaymentsDriver}: no other gateway here separates the two, and the
203
+ * canonical `BillingStatus` has no `authorized` member to hang it off. It is public
204
+ * because the alternative is worse — an account with auto-capture switched off would have
205
+ * `pending` payments that this package could never settle, and Razorpay voids an
206
+ * uncaptured authorization on its own after a few days.
207
+ *
208
+ * `amount` must equal the authorized amount; Razorpay rejects a partial capture.
209
+ */
210
+ async capturePayment(paymentGatewayId, amount, currency) {
211
+ const data = await this.#request(`/payments/${paymentGatewayId}/capture`, {
212
+ method: 'POST',
213
+ body: { amount, currency: (currency ?? this.#currency).toUpperCase() },
214
+ });
215
+ const payment = this.#mapPayment(data);
216
+ publishPaymentDiagnostics(payment);
217
+ return payment;
218
+ }
219
+ // ── Checkout ─────────────────────────────────────────────────────────────────────────
220
+ async createCheckout(input) {
221
+ if (input.cancelUrl !== undefined) {
222
+ throw new Error('[payments] A Razorpay Payment Link has one redirect (`callback_url`) and no cancel ' +
223
+ 'URL — a payer who abandons it simply stays on the link. Drop `cancelUrl` rather ' +
224
+ 'than letting it look configured.');
225
+ }
226
+ if (input.planId !== undefined || input.trialDays !== undefined) {
227
+ throw new Error('[payments] Razorpay does not create subscriptions through a Payment Link. Call ' +
228
+ '`createSubscription()` instead — the hosted authorization link comes back on the ' +
229
+ "subscription's `short_url` (in `subscription.payload`).");
230
+ }
231
+ if (input.idempotencyKey !== undefined && input.idempotencyKey.length > RECEIPT_MAX_LENGTH) {
232
+ throw new Error(`[payments] Razorpay carries \`idempotencyKey\` as the link's \`reference_id\`, capped at ${RECEIPT_MAX_LENGTH} characters; got ${input.idempotencyKey.length}.`);
233
+ }
234
+ if (input.externalReference !== undefined &&
235
+ input.externalReference.length > RECEIPT_MAX_LENGTH) {
236
+ throw new Error(`[payments] Razorpay carries \`externalReference\` as the link's \`reference_id\`, capped at ${RECEIPT_MAX_LENGTH} characters; got ${input.externalReference.length}.`);
237
+ }
238
+ const notes = this.#notes(input.externalReference, {
239
+ ...(input.customerId !== undefined ? { customer_id: input.customerId } : {}),
240
+ ...input.metadata,
241
+ });
242
+ const reference = input.externalReference ?? input.idempotencyKey;
243
+ const body = {
244
+ // Paise, straight through — same integer unit on both sides, nothing to convert.
245
+ amount: input.amount,
246
+ currency: (input.currency ?? this.#currency).toUpperCase(),
247
+ ...(input.description !== undefined ? { description: input.description } : {}),
248
+ // `reference_id` is echoed on `payment_link.paid`, so it is the routing key here —
249
+ // and it is also unique per link, which makes it Razorpay's idempotency for links.
250
+ ...(reference !== undefined ? { reference_id: reference } : {}),
251
+ callback_url: input.successUrl,
252
+ callback_method: 'get',
253
+ ...(notes !== undefined ? { notes } : {}),
254
+ };
255
+ const data = await this.#request('/payment_links', {
256
+ method: 'POST',
257
+ body,
258
+ });
259
+ return {
260
+ id: data.id,
261
+ gatewayId: data.id,
262
+ provider: this.provider,
263
+ url: data.short_url,
264
+ status: data.status === 'paid' ? 'complete' : data.status === 'expired' ? 'expired' : 'open',
265
+ amount: { amount: data.amount, currency: data.currency.toLowerCase() },
266
+ ...(input.customerId !== undefined ? { customerId: input.customerId } : {}),
267
+ };
268
+ }
269
+ // ── Subscriptions ────────────────────────────────────────────────────────────────────
270
+ async createSubscription(input) {
271
+ this.#refuseIdempotencyKey(input.idempotencyKey, 'subscription creation', 'POST /v1/subscriptions');
272
+ if (input.amount !== undefined) {
273
+ throw new Error('[payments] Razorpay prices a subscription from its plan, not from the call. Remove ' +
274
+ '`amount` and point `planId` at a plan created with that amount (`POST /v1/plans`); ' +
275
+ 'passing it here would be a price the gateway never applies.');
276
+ }
277
+ if (input.cycle !== undefined) {
278
+ throw new Error('[payments] Razorpay takes the billing cycle from the plan (`period`/`interval`), not ' +
279
+ 'from the subscription. Remove `cycle` and use a plan with that cycle.');
280
+ }
281
+ if (input.method !== undefined) {
282
+ throw new Error('[payments] Razorpay does not let the API pin the mandate method — the payer picks ' +
283
+ 'card, UPI or e-mandate on the hosted authorization link. Remove `method`.');
284
+ }
285
+ if (input.card !== undefined) {
286
+ throw new Error('[payments] Razorpay authorizes a subscription mandate on its own hosted link, not ' +
287
+ 'from a card token you hold. Remove `card` and send the payer to the subscription ' +
288
+ '`short_url`.');
289
+ }
290
+ // Razorpay has no "until cancelled" subscription: `total_count` is required and fixes
291
+ // how many cycles are billed. There is no field for it on CreateSubscriptionInput, so
292
+ // it comes through the provider-specific escape hatch and is refused when absent —
293
+ // guessing a number here would silently cap (or over-run) the customer's billing.
294
+ const totalCount = input.metadata?.totalCount;
295
+ if (typeof totalCount !== 'number' || !Number.isInteger(totalCount) || totalCount < 1) {
296
+ throw new Error('[payments] Razorpay requires the number of billing cycles up front. Pass it as ' +
297
+ '`metadata: { totalCount: 12 }` — the API has no open-ended subscription.');
298
+ }
299
+ const startAt = this.#subscriptionStart(input);
300
+ const notes = this.#notes(input.externalReference, {
301
+ ...(input.customerId !== undefined ? { customer_id: input.customerId } : {}),
302
+ ...(input.description !== undefined ? { description: input.description } : {}),
303
+ });
304
+ const body = {
305
+ plan_id: input.planId,
306
+ total_count: totalCount,
307
+ ...(typeof input.metadata?.quantity === 'number'
308
+ ? { quantity: input.metadata.quantity }
309
+ : {}),
310
+ ...(startAt !== undefined ? { start_at: startAt } : {}),
311
+ ...(input.customer?.email !== undefined
312
+ ? { customer_notify: true, notify_info: { notify_email: input.customer.email } }
313
+ : {}),
314
+ ...(notes !== undefined ? { notes } : {}),
315
+ };
316
+ const data = await this.#request('/subscriptions', {
317
+ method: 'POST',
318
+ body,
319
+ });
320
+ const subscription = this.#mapSubscription(data, input.customerId);
321
+ publishSubscriptionDiagnostics(subscription, 'subscription.created');
322
+ return subscription;
323
+ }
324
+ async cancelSubscription(subscriptionGatewayId, options) {
325
+ const data = await this.#request(`/subscriptions/${subscriptionGatewayId}/cancel`, { method: 'POST', body: { cancel_at_cycle_end: options?.atPeriodEnd === true } });
326
+ const subscription = this.#mapSubscription(data);
327
+ publishSubscriptionDiagnostics(subscription, 'subscription.canceled');
328
+ return subscription;
329
+ }
330
+ async updateSubscription(subscriptionGatewayId, input) {
331
+ this.#refuseIdempotencyKey(input.idempotencyKey, 'a subscription update', 'PATCH /v1/subscriptions/:id');
332
+ if (input.amount !== undefined) {
333
+ throw new Error('[payments] Razorpay cannot change a subscription amount: the price lives on the plan ' +
334
+ 'and plans are immutable. Create a new plan and swap it with ' +
335
+ '`metadata: { planId: "plan_…" }`, which is a real, billed change.');
336
+ }
337
+ if (input.description !== undefined) {
338
+ throw new Error('[payments] A Razorpay subscription has no description, and `PATCH /v1/subscriptions` ' +
339
+ 'does not update `notes`. Keep the description on your own record.');
340
+ }
341
+ const planId = input.metadata?.planId;
342
+ const quantity = input.metadata?.quantity;
343
+ if (typeof planId !== 'string' && typeof quantity !== 'number') {
344
+ throw new Error('[payments] Nothing to update on a Razorpay subscription. It accepts a plan swap ' +
345
+ '(`metadata: { planId }`) or a quantity change (`metadata: { quantity }`); anything ' +
346
+ 'else would be a change the gateway never sees.');
347
+ }
348
+ const body = {
349
+ ...(typeof planId === 'string' ? { plan_id: planId } : {}),
350
+ ...(typeof quantity === 'number' ? { quantity } : {}),
351
+ // Razorpay otherwise schedules the change for the next cycle and answers with the
352
+ // *old* subscription, which reads exactly like a silent no-op.
353
+ schedule_change_at: typeof input.metadata?.scheduleChangeAt === 'string'
354
+ ? input.metadata.scheduleChangeAt
355
+ : 'now',
356
+ };
357
+ const data = await this.#request(`/subscriptions/${subscriptionGatewayId}`, { method: 'PATCH', body });
358
+ // No diagnostics event here: `publishSubscriptionDiagnostics` only knows
359
+ // `subscription.created` and `subscription.canceled`, and inventing a third would mean
360
+ // widening a shared type this driver does not own.
361
+ return this.#mapSubscription(data);
362
+ }
363
+ async findSubscription(gatewayId) {
364
+ try {
365
+ const data = await this.#request(`/subscriptions/${gatewayId}`);
366
+ return this.#mapSubscription(data);
367
+ }
368
+ catch (error) {
369
+ if (isNotFound(error))
370
+ return null;
371
+ throw error;
372
+ }
373
+ }
374
+ // ── Invoices ─────────────────────────────────────────────────────────────────────────
375
+ async listInvoices(customerId) {
376
+ const data = await this.#request(`/invoices?customer_id=${encodeURIComponent(customerId)}&count=100`);
377
+ // `customer_id` is a documented filter on this endpoint, and the same check is repeated
378
+ // locally so a silently-ignored query parameter cannot hand one tenant another's
379
+ // invoices. `count` caps at 100 and this driver does not paginate past it.
380
+ return (data.items ?? [])
381
+ .filter((invoice) => invoice.customer_id === customerId)
382
+ .map((invoice) => this.#mapInvoice(invoice));
383
+ }
384
+ // ── Webhooks ─────────────────────────────────────────────────────────────────────────
385
+ /**
386
+ * Whether a delivery to `POST /payments/webhook/:provider` can be authenticated.
387
+ *
388
+ * Without the secret the `X-Razorpay-Signature` header is never checked.
389
+ */
390
+ get webhookVerification() {
391
+ return this.#webhookSecret !== undefined ? 'configured' : 'unconfigured';
392
+ }
393
+ parseWebhook(rawBody, headers) {
394
+ if (this.#webhookSecret !== undefined) {
395
+ const signature = headerValue(headers, 'x-razorpay-signature');
396
+ if (signature === undefined || signature === '') {
397
+ throw new Error('[payments] Missing X-Razorpay-Signature on Razorpay webhook request.');
398
+ }
399
+ // Razorpay hex-encodes the digest; the shared helper compares it timing-safe.
400
+ if (!verifyHmacSignature(rawBody, signature, this.#webhookSecret, 'sha256', 'hex')) {
401
+ throw new Error('[payments] Invalid Razorpay webhook signature.');
402
+ }
403
+ }
404
+ const payload = JSON.parse(rawBody);
405
+ const entity = payload.payload?.payment?.entity ??
406
+ payload.payload?.payment_link?.entity ??
407
+ payload.payload?.order?.entity ??
408
+ payload.payload?.subscription?.entity ??
409
+ payload.payload?.refund?.entity ??
410
+ payload.payload?.dispute?.entity;
411
+ return {
412
+ id: headerValue(headers, 'x-razorpay-event-id') ?? `${payload.event}-${entity?.id ?? ''}`,
413
+ provider: this.provider,
414
+ type: this.#mapWebhookType(payload),
415
+ createdAt: this.#toIso(payload.created_at) ?? new Date().toISOString(),
416
+ data: this.#mapWebhookData(payload),
417
+ raw: payload,
418
+ };
419
+ }
420
+ // ── Mappers ──────────────────────────────────────────────────────────────────────────
421
+ #mapCustomer(data) {
422
+ const notes = this.#readNotes(data.notes);
423
+ return {
424
+ id: data.id,
425
+ ...(data.name ? { name: data.name } : {}),
426
+ ...(data.email ? { email: data.email } : {}),
427
+ ...(data.gstin ? { taxId: data.gstin } : notes?.tax_id ? { taxId: notes.tax_id } : {}),
428
+ };
429
+ }
430
+ /**
431
+ * An order rendered as a `Payment`. An order is an amount Razorpay expects to collect,
432
+ * so it is `pending` until a payment against it succeeds; `paid` is the only settled
433
+ * state (`attempted` means someone tried and it did not go through).
434
+ */
435
+ #mapOrder(data, customerId) {
436
+ const notes = this.#readNotes(data.notes);
437
+ const result = {
438
+ id: data.id,
439
+ gatewayId: data.id,
440
+ provider: this.provider,
441
+ amount: { amount: data.amount, currency: data.currency.toLowerCase() },
442
+ status: data.status === 'paid' ? 'paid' : 'pending',
443
+ payload: data,
444
+ createdAt: this.#toIso(data.created_at) ?? new Date().toISOString(),
445
+ };
446
+ const owner = customerId ?? notes?.customer_id;
447
+ if (owner !== undefined)
448
+ result.customerId = owner;
449
+ return result;
450
+ }
451
+ #mapPayment(data) {
452
+ const method = this.#mapMethodToType(data);
453
+ const result = {
454
+ id: data.id,
455
+ gatewayId: data.id,
456
+ provider: this.provider,
457
+ amount: { amount: data.amount, currency: data.currency.toLowerCase() },
458
+ status: this.#mapPaymentStatus(data.status),
459
+ payload: data,
460
+ createdAt: this.#toIso(data.created_at) ?? new Date().toISOString(),
461
+ };
462
+ if (method !== undefined)
463
+ result.method = method;
464
+ if (data.customer_id)
465
+ result.customerId = data.customer_id;
466
+ if (data.status === 'captured') {
467
+ const paidAt = this.#toIso(data.created_at);
468
+ if (paidAt !== null)
469
+ result.paidAt = paidAt;
470
+ }
471
+ return result;
472
+ }
473
+ #mapPaymentLink(data) {
474
+ const statusMap = {
475
+ created: 'pending',
476
+ partially_paid: 'pending',
477
+ paid: 'paid',
478
+ cancelled: 'canceled',
479
+ expired: 'failed',
480
+ };
481
+ return {
482
+ id: data.id,
483
+ gatewayId: data.id,
484
+ provider: this.provider,
485
+ amount: { amount: data.amount, currency: data.currency.toLowerCase() },
486
+ status: statusMap[data.status] ?? 'pending',
487
+ payload: data,
488
+ createdAt: this.#toIso(data.created_at) ?? new Date().toISOString(),
489
+ hostedUrl: data.short_url,
490
+ };
491
+ }
492
+ /**
493
+ * `authorized` means the funds are held on the payer's instrument and nothing has moved
494
+ * to the merchant yet — Razorpay voids the hold on its own if it is never captured.
495
+ *
496
+ * It used to answer `pending`, and the driver said so on purpose: of the statuses that
497
+ * existed, `pending` was the only one meaning "not settled, not failed". It understated
498
+ * the case, though — a `pending` payment is one nobody has attempted, while an
499
+ * authorized one has the payer's money reserved and a clock running on it. `authorized`
500
+ * is the word now, and it still is not `paid`: this is Razorpay's authorize-then-capture
501
+ * split, the clearest case of it in this package.
502
+ */
503
+ #mapPaymentStatus(status) {
504
+ switch (status) {
505
+ case 'captured':
506
+ return 'paid';
507
+ case 'authorized':
508
+ return 'authorized';
509
+ case 'refunded':
510
+ return 'refunded';
511
+ case 'failed':
512
+ return 'failed';
513
+ default:
514
+ return 'pending';
515
+ }
516
+ }
517
+ /**
518
+ * Razorpay's `method` → the canonical {@link PaymentMethodType}.
519
+ *
520
+ * UPI is how most Indian payers actually pay, and it had no name here: the driver left
521
+ * it unset rather than borrow `pix`, which would have put a Brazilian label on an Indian
522
+ * payment. `upi` is a member of the union now — named outright, not as a local
523
+ * alternative — so it is reported as itself.
524
+ *
525
+ * The rest go by category: netbanking pushes from the payer's own bank, `paylater` and
526
+ * `cardless_emi` are buy-now-pay-later, and `emi` is a card instalment plan (Razorpay
527
+ * returns the card details with it), so it is a card.
528
+ */
529
+ #mapMethodToType(data) {
530
+ switch (data.method) {
531
+ case 'card':
532
+ case 'emi':
533
+ return data.card?.type === 'debit' ? 'debit_card' : 'card';
534
+ case 'upi':
535
+ return 'upi';
536
+ case 'wallet':
537
+ return 'wallet';
538
+ case 'netbanking':
539
+ return 'bank_transfer';
540
+ case 'paylater':
541
+ case 'cardless_emi':
542
+ return 'bnpl';
543
+ default:
544
+ return undefined;
545
+ }
546
+ }
547
+ #mapSubscription(data, customerId) {
548
+ const statusMap = {
549
+ created: 'incomplete',
550
+ authenticated: 'trialing',
551
+ active: 'active',
552
+ pending: 'past_due',
553
+ halted: 'past_due',
554
+ // A paused Razorpay subscription is not in arrears — it exists, bills nothing today
555
+ // and resumes later. `past_due` said the subscriber owed money they did not.
556
+ paused: 'paused',
557
+ cancelled: 'canceled',
558
+ completed: 'ended',
559
+ expired: 'ended',
560
+ };
561
+ const notes = this.#readNotes(data.notes);
562
+ const result = {
563
+ id: data.id,
564
+ gatewayId: data.id,
565
+ provider: this.provider,
566
+ customerId: data.customer_id ?? customerId ?? notes?.customer_id ?? '',
567
+ status: statusMap[data.status] ?? 'active',
568
+ planId: data.plan_id,
569
+ payload: data,
570
+ createdAt: this.#toIso(data.created_at) ?? new Date().toISOString(),
571
+ };
572
+ const currentStart = this.#toIso(data.current_start);
573
+ const currentEnd = this.#toIso(data.current_end);
574
+ const endedAt = this.#toIso(data.ended_at);
575
+ if (currentStart !== null)
576
+ result.currentPeriodStart = currentStart;
577
+ if (currentEnd !== null)
578
+ result.currentPeriodEnd = currentEnd;
579
+ if (endedAt !== null)
580
+ result.endsAt = endedAt;
581
+ return result;
582
+ }
583
+ #mapInvoice(data) {
584
+ const statusMap = {
585
+ draft: 'draft',
586
+ issued: 'open',
587
+ partially_paid: 'open',
588
+ paid: 'paid',
589
+ cancelled: 'canceled',
590
+ expired: 'void',
591
+ deleted: 'void',
592
+ };
593
+ const issuedAt = this.#toIso(data.issued_at);
594
+ return {
595
+ id: data.id,
596
+ gatewayId: data.id,
597
+ provider: this.provider,
598
+ ...(data.customer_id ? { customerId: data.customer_id } : {}),
599
+ ...(data.subscription_id ? { subscriptionId: data.subscription_id } : {}),
600
+ status: statusMap[data.status] ?? 'draft',
601
+ amount: { amount: data.amount, currency: data.currency.toLowerCase() },
602
+ createdAt: this.#toIso(data.created_at) ?? new Date().toISOString(),
603
+ ...(data.short_url ? { hostedPdfUrl: data.short_url } : {}),
604
+ ...(data.invoice_number ? { number: data.invoice_number } : {}),
605
+ ...(issuedAt !== null ? { issuedAt } : {}),
606
+ payload: data,
607
+ };
608
+ }
609
+ #mapWebhookType(payload) {
610
+ const dispute = payload.payload?.dispute?.entity;
611
+ switch (payload.event) {
612
+ case 'payment.captured':
613
+ case 'order.paid':
614
+ case 'payment_link.paid':
615
+ case 'subscription.charged':
616
+ return 'payment.succeeded';
617
+ case 'payment.failed':
618
+ case 'payment_link.expired':
619
+ return 'payment.failed';
620
+ case 'refund.processed':
621
+ return 'payment.refunded';
622
+ // ── The dispute family ───────────────────────────────────────────────────────────
623
+ // `payment.dispute.created` fires for all five of Razorpay's dispute **phases**, and
624
+ // the first two are not a chargeback. `fraud` is "a dispute raised by the bank when
625
+ // it suspects a transaction to be fraudulent based on the risk analysis" — the
626
+ // issuer's TC40/SAFE alert — and `retrieval` is "a request initiated by the customer
627
+ // with their issuer bank for additional information about a transaction", which
628
+ // Razorpay's own guide calls "essentially a *soft* chargeback". Neither has taken any
629
+ // money, and Razorpay's advice is to act during exactly these phases; calling them
630
+ // `payment.disputed` moves a paid row over money still in the account.
631
+ //
632
+ // `chargeback` and the two appeal phases are the refund claim with the bank's
633
+ // official inquiry open, which is what `payment.disputed` names. With no phase on the
634
+ // entity this keeps the mapping it has always had rather than downgrading a dispute
635
+ // the driver could not read.
636
+ case 'payment.dispute.created':
637
+ return dispute?.phase === 'fraud' || dispute?.phase === 'retrieval'
638
+ ? 'payment.dispute_warning'
639
+ : 'payment.disputed';
640
+ // The three outcomes. Each names its own result, so the outcome is readable from the
641
+ // event alone — but only if the envelope carried the dispute it is about; without one
642
+ // there is no `disputeId`, no amount and nothing to close, so it degrades to an
643
+ // update rather than emitting a close the processor would reject.
644
+ case 'payment.dispute.won':
645
+ case 'payment.dispute.lost':
646
+ case 'payment.dispute.closed':
647
+ return dispute === undefined ? 'payment.updated' : 'payment.dispute_closed';
648
+ // `payment.authorized` is money held, not money moved — reporting it as a success
649
+ // would have the billing layer settle an order the merchant has not been paid for,
650
+ // and there is deliberately no `payment.authorized` event for it to become.
651
+ case 'payment.authorized':
652
+ // Movement inside an open dispute, not a resolution: `under_review` is the bank
653
+ // reviewing the evidence you submitted, `action_required` is Razorpay saying that
654
+ // evidence was insufficient or unreadable and has to be resubmitted.
655
+ case 'payment.dispute.under_review':
656
+ case 'payment.dispute.action_required':
657
+ case 'refund.created':
658
+ case 'refund.failed':
659
+ case 'payment_link.cancelled':
660
+ case 'payment_link.partially_paid':
661
+ return 'payment.updated';
662
+ case 'subscription.authenticated':
663
+ return 'subscription.created';
664
+ case 'subscription.cancelled':
665
+ return 'subscription.canceled';
666
+ case 'subscription.activated':
667
+ case 'subscription.updated':
668
+ case 'subscription.pending':
669
+ case 'subscription.halted':
670
+ case 'subscription.paused':
671
+ case 'subscription.resumed':
672
+ case 'subscription.completed':
673
+ return 'subscription.updated';
674
+ default:
675
+ return payload.event;
676
+ }
677
+ }
678
+ /**
679
+ * The dispute event name → the canonical outcome, or `undefined` for everything that is
680
+ * not a resolution.
681
+ *
682
+ * Razorpay names the result in the event itself, and its status descriptions say the same
683
+ * thing: `won` is "the bank has accepted the remedial documents", `lost` is "the bank did
684
+ * not accept" them. `closed` is the odd one — "a fraudulent transaction is closed after
685
+ * you provide details of the transaction or make a refund to the customer. This is seen
686
+ * in fraudulent transactions only" — so no verdict was reached and no chargeback amount
687
+ * was ever deducted. That is `canceled`, not `won`: nothing was decided in your favour,
688
+ * the case simply stopped existing.
689
+ */
690
+ #disputeOutcome(event) {
691
+ switch (event) {
692
+ case 'payment.dispute.won':
693
+ return 'won';
694
+ case 'payment.dispute.lost':
695
+ return 'lost';
696
+ case 'payment.dispute.closed':
697
+ return 'canceled';
698
+ default:
699
+ return undefined;
700
+ }
701
+ }
702
+ #mapWebhookData(payload) {
703
+ const disputeEntity = payload.payload?.dispute?.entity;
704
+ if (disputeEntity !== undefined) {
705
+ // A dispute envelope carries the payment entity alongside the dispute, and the two
706
+ // disagree about the money: `payment.amount` is what was charged, `dispute.amount`
707
+ // what is being pulled back. The row is keyed on the PAYMENT — that is what has to
708
+ // stop saying `paid` — and the amount is the disputed one, with the reason, the phase
709
+ // and the deadline left on `event.raw`.
710
+ const reference = this.#readNotes(payload.payload?.payment?.entity?.notes)?.external_reference;
711
+ const respondBy = this.#toIso(disputeEntity.respond_by);
712
+ const outcome = this.#disputeOutcome(payload.event);
713
+ return {
714
+ gatewayId: disputeEntity.payment_id,
715
+ amount: disputeEntity.amount,
716
+ currency: disputeEntity.currency.toLowerCase(),
717
+ disputeId: disputeEntity.id,
718
+ // `respond_by` is "the Unix timestamp by which a response should be sent" — the one
719
+ // field that makes a fraud alert or a retrieval request actionable, and the reason
720
+ // acting during those phases avoids the chargeback entirely.
721
+ ...(respondBy !== null ? { actionableUntil: respondBy } : {}),
722
+ ...(disputeEntity.reason_code !== undefined ? { reason: disputeEntity.reason_code } : {}),
723
+ ...(disputeEntity.status !== undefined ? { disputeStatus: disputeEntity.status } : {}),
724
+ ...(disputeEntity.phase !== undefined ? { disputePhase: disputeEntity.phase } : {}),
725
+ // `amount_deducted` is Razorpay's own record of what actually left the balance, and
726
+ // it stays 0 until the dispute is lost. Passed through so a handler never has to
727
+ // infer the money movement from the event name.
728
+ ...(disputeEntity.amount_deducted !== undefined
729
+ ? { amountDeducted: disputeEntity.amount_deducted }
730
+ : {}),
731
+ ...(outcome !== undefined ? { outcome } : {}),
732
+ ...(reference !== undefined ? { externalReference: reference } : {}),
733
+ };
734
+ }
735
+ const paymentEntity = payload.payload?.payment?.entity;
736
+ const linkEntity = payload.payload?.payment_link?.entity;
737
+ const orderEntity = payload.payload?.order?.entity;
738
+ const subscriptionEntity = payload.payload?.subscription?.entity;
739
+ const refundEntity = payload.payload?.refund?.entity;
740
+ // The app's own reference can arrive on any of the entities the event carries: notes on
741
+ // the order/payment/subscription, `reference_id` on a payment link. The API reference
742
+ // does not state that an order's notes are copied onto the payment it produces, so
743
+ // every entity in the envelope is checked rather than trusting one of them.
744
+ const externalReference = this.#readNotes(paymentEntity?.notes)?.external_reference ??
745
+ linkEntity?.reference_id ??
746
+ this.#readNotes(orderEntity?.notes)?.external_reference ??
747
+ this.#readNotes(subscriptionEntity?.notes)?.external_reference ??
748
+ this.#readNotes(linkEntity?.notes)?.external_reference;
749
+ if (subscriptionEntity !== undefined && paymentEntity === undefined) {
750
+ const subscription = this.#mapSubscription(subscriptionEntity);
751
+ return {
752
+ gatewayId: subscription.gatewayId,
753
+ customerId: subscription.customerId,
754
+ status: subscription.status,
755
+ planId: subscription.planId,
756
+ ...(subscription.endsAt !== undefined ? { endsAt: subscription.endsAt } : {}),
757
+ ...(externalReference !== undefined ? { externalReference } : {}),
758
+ };
759
+ }
760
+ const source = paymentEntity ?? linkEntity ?? orderEntity ?? refundEntity;
761
+ if (source === undefined)
762
+ return {};
763
+ const payment = paymentEntity !== undefined
764
+ ? this.#mapPayment(paymentEntity)
765
+ : linkEntity !== undefined
766
+ ? this.#mapPaymentLink(linkEntity)
767
+ : orderEntity !== undefined
768
+ ? this.#mapOrder(orderEntity)
769
+ : undefined;
770
+ return {
771
+ gatewayId: source.id,
772
+ amount: source.amount,
773
+ currency: source.currency.toLowerCase(),
774
+ ...(payment?.customerId !== undefined ? { customerId: payment.customerId } : {}),
775
+ ...(paymentEntity?.order_id ? { orderId: paymentEntity.order_id } : {}),
776
+ ...(subscriptionEntity !== undefined
777
+ ? { subscriptionId: subscriptionEntity.id, planId: subscriptionEntity.plan_id }
778
+ : {}),
779
+ ...(externalReference !== undefined ? { externalReference } : {}),
780
+ };
781
+ }
782
+ // ── Helpers ──────────────────────────────────────────────────────────────────────────
783
+ /**
784
+ * Razorpay answers with `"notes": []` — an empty *array* — when an entity has none, so
785
+ * every read goes through here rather than treating it as an object.
786
+ */
787
+ #readNotes(notes) {
788
+ if (notes === null || notes === undefined || Array.isArray(notes))
789
+ return undefined;
790
+ return notes;
791
+ }
792
+ /** Build the `notes` object, with `externalReference` under a stable key. */
793
+ #notes(externalReference, extra) {
794
+ const notes = {};
795
+ for (const [key, value] of Object.entries(extra)) {
796
+ if (value !== undefined && value !== null)
797
+ notes[key] = String(value);
798
+ }
799
+ if (externalReference !== undefined)
800
+ notes.external_reference = externalReference;
801
+ return Object.keys(notes).length > 0 ? notes : undefined;
802
+ }
803
+ /** `startDate` wins over `trialDays`; both become Razorpay's Unix-second `start_at`. */
804
+ #subscriptionStart(input) {
805
+ if (input.startDate !== undefined) {
806
+ const date = new Date(input.startDate);
807
+ if (Number.isNaN(date.getTime())) {
808
+ throw new Error(`[payments] Razorpay: \`startDate\` "${input.startDate}" is not a date.`);
809
+ }
810
+ return Math.floor(date.getTime() / 1000);
811
+ }
812
+ if (input.trialDays !== undefined) {
813
+ return Math.floor((Date.now() + input.trialDays * 24 * 60 * 60 * 1000) / 1000);
814
+ }
815
+ return undefined;
816
+ }
817
+ /**
818
+ * Razorpay's idempotency, such as it is: **refunds only**, under a header of their own
819
+ * naming (`X-Refund-Idempotency`), keyed at ten characters or more of alphanumerics,
820
+ * hyphens and underscores. There is no `Idempotency-Key` anywhere in the API, and the
821
+ * retry must repeat the same body or Razorpay rejects it as a `BAD_REQUEST`.
822
+ */
823
+ #refundIdempotency(key) {
824
+ if (key === undefined)
825
+ return {};
826
+ if (key.length < REFUND_IDEMPOTENCY_MIN_LENGTH) {
827
+ throw new Error(`[payments] Razorpay requires an \`X-Refund-Idempotency\` key of at least ${REFUND_IDEMPOTENCY_MIN_LENGTH} characters; got ${key.length}. A key it rejects is a key that does not deduplicate.`);
828
+ }
829
+ if (!/^[A-Za-z0-9_-]+$/.test(key)) {
830
+ throw new Error('[payments] Razorpay accepts only letters, numbers, hyphens and underscores in `X-Refund-Idempotency`; the key given has something else in it.');
831
+ }
832
+ return { headers: { 'X-Refund-Idempotency': key } };
833
+ }
834
+ /**
835
+ * Refuse an `idempotencyKey` on an operation Razorpay does not deduplicate.
836
+ *
837
+ * Razorpay has no general idempotency header: refunds have `X-Refund-Idempotency`,
838
+ * RazorpayX payouts have `X-Payout-Idempotency`, and customers and subscriptions have
839
+ * nothing at all. Accepting a key on those and dropping it would turn the caller's retry
840
+ * guarantee into a second customer, or a second live subscription billing the same
841
+ * person — which is the whole reason the contract says to throw instead.
842
+ */
843
+ #refuseIdempotencyKey(key, operation, endpoint) {
844
+ if (key === undefined)
845
+ return;
846
+ throw new Error(`[payments] Razorpay does not deduplicate ${operation}: \`${endpoint}\` documents no idempotency key, and Razorpay has no general \`Idempotency-Key\` header — only refunds (\`X-Refund-Idempotency\`) and RazorpayX payouts have one. Drop \`idempotencyKey\` and make the call safe on your side, because silently ignoring it would turn a retry into a second one.`);
847
+ }
848
+ /** Razorpay timestamps are Unix seconds. */
849
+ #toIso(value) {
850
+ if (value === null || value === undefined || value === 0)
851
+ return null;
852
+ const date = new Date(value * 1000);
853
+ if (Number.isNaN(date.getTime()))
854
+ return null;
855
+ return date.toISOString();
856
+ }
857
+ async #request(path, options = {}) {
858
+ return httpRequest(path, {
859
+ baseUrl: this.#baseUrl,
860
+ ...(options.method !== undefined ? { method: options.method } : {}),
861
+ ...(options.body !== undefined ? { body: options.body } : {}),
862
+ ...(options.headers !== undefined ? { headers: options.headers } : {}),
863
+ authHeader: this.#authHeader,
864
+ });
865
+ }
866
+ }
867
+ //# sourceMappingURL=razorpay.js.map