@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.
- package/CHANGELOG.md +2375 -0
- package/dist/assets/spa/assets/index-B7rU8eIL.css +2 -0
- package/dist/assets/spa/assets/index-BcM2VH5k.js +10 -0
- package/dist/assets/spa/assets/index-BcM2VH5k.js.map +1 -0
- package/dist/assets/spa/index.html +20 -0
- package/dist/commands/main.d.ts +4 -3
- package/dist/commands/main.d.ts.map +1 -1
- package/dist/commands/main.js +9 -2
- package/dist/commands/main.js.map +1 -1
- package/dist/commands/make_billable.d.ts.map +1 -1
- package/dist/commands/make_billable.js +1 -1
- package/dist/commands/make_billable.js.map +1 -1
- package/dist/commands/make_webhook_handler.d.ts.map +1 -1
- package/dist/commands/make_webhook_handler.js +1 -1
- package/dist/commands/make_webhook_handler.js.map +1 -1
- package/dist/commands/payments_health.d.ts +31 -0
- package/dist/commands/payments_health.d.ts.map +1 -0
- package/dist/commands/payments_health.js +113 -0
- package/dist/commands/payments_health.js.map +1 -0
- package/dist/commands/payments_sync.d.ts +18 -4
- package/dist/commands/payments_sync.d.ts.map +1 -1
- package/dist/commands/payments_sync.js +136 -40
- package/dist/commands/payments_sync.js.map +1 -1
- package/dist/commands/payments_webhook.d.ts.map +1 -1
- package/dist/commands/payments_webhook.js.map +1 -1
- package/dist/configure.d.ts +12 -5
- package/dist/configure.d.ts.map +1 -1
- package/dist/configure.js +18 -5
- package/dist/configure.js.map +1 -1
- package/dist/providers/dashboard_provider.d.ts +97 -0
- package/dist/providers/dashboard_provider.d.ts.map +1 -0
- package/dist/providers/dashboard_provider.js +430 -0
- package/dist/providers/dashboard_provider.js.map +1 -0
- package/dist/providers/payments_client_provider.d.ts +41 -0
- package/dist/providers/payments_client_provider.d.ts.map +1 -0
- package/dist/providers/payments_client_provider.js +95 -0
- package/dist/providers/payments_client_provider.js.map +1 -0
- package/dist/providers/payments_provider.d.ts +35 -0
- package/dist/providers/payments_provider.d.ts.map +1 -1
- package/dist/providers/payments_provider.js +250 -36
- package/dist/providers/payments_provider.js.map +1 -1
- package/dist/src/billing/billing_health.d.ts +85 -0
- package/dist/src/billing/billing_health.d.ts.map +1 -0
- package/dist/src/billing/billing_health.js +155 -0
- package/dist/src/billing/billing_health.js.map +1 -0
- package/dist/src/billing/billing_overview.d.ts +4 -3
- package/dist/src/billing/billing_overview.d.ts.map +1 -1
- package/dist/src/billing/billing_overview.js +14 -5
- package/dist/src/billing/billing_overview.js.map +1 -1
- package/dist/src/billing/billing_store.d.ts +634 -5
- package/dist/src/billing/billing_store.d.ts.map +1 -1
- package/dist/src/billing/billing_store.js +18 -1
- package/dist/src/billing/billing_store.js.map +1 -1
- package/dist/src/billing/index.d.ts +4 -3
- package/dist/src/billing/index.d.ts.map +1 -1
- package/dist/src/billing/index.js +2 -1
- package/dist/src/billing/index.js.map +1 -1
- package/dist/src/billing/list_query.d.ts +16 -0
- package/dist/src/billing/list_query.d.ts.map +1 -0
- package/dist/src/billing/list_query.js +28 -0
- package/dist/src/billing/list_query.js.map +1 -0
- package/dist/src/billing/lucid_billing_store.d.ts +122 -4
- package/dist/src/billing/lucid_billing_store.d.ts.map +1 -1
- package/dist/src/billing/lucid_billing_store.js +811 -13
- package/dist/src/billing/lucid_billing_store.js.map +1 -1
- package/dist/src/billing/mixins/index.d.ts +5 -2
- package/dist/src/billing/mixins/index.d.ts.map +1 -1
- package/dist/src/billing/mixins/index.js +5 -2
- package/dist/src/billing/mixins/index.js.map +1 -1
- package/dist/src/billing/mixins/with_audit_event.d.ts +48 -0
- package/dist/src/billing/mixins/with_audit_event.d.ts.map +1 -0
- package/dist/src/billing/mixins/with_audit_event.js +77 -0
- package/dist/src/billing/mixins/with_audit_event.js.map +1 -0
- package/dist/src/billing/mixins/with_billable.d.ts.map +1 -1
- package/dist/src/billing/mixins/with_customer.d.ts +39 -0
- package/dist/src/billing/mixins/with_customer.d.ts.map +1 -0
- package/dist/src/billing/mixins/with_customer.js +68 -0
- package/dist/src/billing/mixins/with_customer.js.map +1 -0
- package/dist/src/billing/mixins/with_dispute.d.ts +68 -0
- package/dist/src/billing/mixins/with_dispute.d.ts.map +1 -0
- package/dist/src/billing/mixins/with_dispute.js +83 -0
- package/dist/src/billing/mixins/with_dispute.js.map +1 -0
- package/dist/src/billing/mixins/with_payment.d.ts +42 -0
- package/dist/src/billing/mixins/with_payment.d.ts.map +1 -1
- package/dist/src/billing/mixins/with_payment.js +29 -3
- package/dist/src/billing/mixins/with_payment.js.map +1 -1
- package/dist/src/billing/mixins/with_subscription.d.ts +7 -0
- package/dist/src/billing/mixins/with_subscription.d.ts.map +1 -1
- package/dist/src/billing/mixins/with_subscription.js +15 -1
- package/dist/src/billing/mixins/with_subscription.js.map +1 -1
- package/dist/src/billing/mixins/with_usage_event.d.ts +7 -0
- package/dist/src/billing/mixins/with_usage_event.d.ts.map +1 -1
- package/dist/src/billing/mixins/with_usage_event.js +15 -1
- package/dist/src/billing/mixins/with_usage_event.js.map +1 -1
- package/dist/src/billing/mixins/with_webhook_event.d.ts +18 -0
- package/dist/src/billing/mixins/with_webhook_event.d.ts.map +1 -1
- package/dist/src/billing/mixins/with_webhook_event.js +18 -1
- package/dist/src/billing/mixins/with_webhook_event.js.map +1 -1
- package/dist/src/billing/resolve_dispatch.d.ts +22 -0
- package/dist/src/billing/resolve_dispatch.d.ts.map +1 -0
- package/dist/src/billing/resolve_dispatch.js +44 -0
- package/dist/src/billing/resolve_dispatch.js.map +1 -0
- package/dist/src/billing/resolve_store.d.ts +18 -0
- package/dist/src/billing/resolve_store.d.ts.map +1 -0
- package/dist/src/billing/resolve_store.js +35 -0
- package/dist/src/billing/resolve_store.js.map +1 -0
- package/dist/src/billing/schema.d.ts +108 -0
- package/dist/src/billing/schema.d.ts.map +1 -0
- package/dist/src/billing/schema.js +350 -0
- package/dist/src/billing/schema.js.map +1 -0
- package/dist/src/billing/webhook_dispatcher.d.ts +95 -16
- package/dist/src/billing/webhook_dispatcher.d.ts.map +1 -1
- package/dist/src/billing/webhook_dispatcher.js +186 -33
- package/dist/src/billing/webhook_dispatcher.js.map +1 -1
- package/dist/src/billing/webhook_events.d.ts +67 -1
- package/dist/src/billing/webhook_events.d.ts.map +1 -1
- package/dist/src/billing/webhook_events.js +15 -0
- package/dist/src/billing/webhook_events.js.map +1 -1
- package/dist/src/billing/webhook_processor.d.ts.map +1 -1
- package/dist/src/billing/webhook_processor.js +325 -5
- package/dist/src/billing/webhook_processor.js.map +1 -1
- package/dist/src/client/define_config.d.ts +150 -0
- package/dist/src/client/define_config.d.ts.map +1 -0
- package/dist/src/client/define_config.js +98 -0
- package/dist/src/client/define_config.js.map +1 -0
- package/dist/src/client/handlers.d.ts +71 -0
- package/dist/src/client/handlers.d.ts.map +1 -0
- package/dist/src/client/handlers.js +119 -0
- package/dist/src/client/handlers.js.map +1 -0
- package/dist/src/client/index.d.ts +15 -0
- package/dist/src/client/index.d.ts.map +1 -0
- package/dist/src/client/index.js +12 -0
- package/dist/src/client/index.js.map +1 -0
- package/dist/src/client/statuses.d.ts +38 -0
- package/dist/src/client/statuses.d.ts.map +1 -0
- package/dist/src/client/statuses.js +42 -0
- package/dist/src/client/statuses.js.map +1 -0
- package/dist/src/dashboard/actions.d.ts +143 -0
- package/dist/src/dashboard/actions.d.ts.map +1 -0
- package/dist/src/dashboard/actions.js +112 -0
- package/dist/src/dashboard/actions.js.map +1 -0
- package/dist/src/dashboard/auth.d.ts +136 -0
- package/dist/src/dashboard/auth.d.ts.map +1 -0
- package/dist/src/dashboard/auth.js +152 -0
- package/dist/src/dashboard/auth.js.map +1 -0
- package/dist/src/dashboard/define_config.d.ts +80 -0
- package/dist/src/dashboard/define_config.d.ts.map +1 -0
- package/dist/src/dashboard/define_config.js +83 -0
- package/dist/src/dashboard/define_config.js.map +1 -0
- package/dist/src/dashboard/handlers.d.ts +281 -0
- package/dist/src/dashboard/handlers.d.ts.map +1 -0
- package/dist/src/dashboard/handlers.js +960 -0
- package/dist/src/dashboard/handlers.js.map +1 -0
- package/dist/src/dashboard/index.d.ts +17 -0
- package/dist/src/dashboard/index.d.ts.map +1 -0
- package/dist/src/dashboard/index.js +13 -0
- package/dist/src/dashboard/index.js.map +1 -0
- package/dist/src/dashboard/login_page.d.ts +23 -0
- package/dist/src/dashboard/login_page.d.ts.map +1 -0
- package/dist/src/dashboard/login_page.js +170 -0
- package/dist/src/dashboard/login_page.js.map +1 -0
- package/dist/src/dashboard/period.d.ts +31 -0
- package/dist/src/dashboard/period.d.ts.map +1 -0
- package/dist/src/dashboard/period.js +44 -0
- package/dist/src/dashboard/period.js.map +1 -0
- package/dist/src/dashboard/session_cookie.d.ts +42 -0
- package/dist/src/dashboard/session_cookie.d.ts.map +1 -0
- package/dist/src/dashboard/session_cookie.js +97 -0
- package/dist/src/dashboard/session_cookie.js.map +1 -0
- package/dist/src/dashboard/spa.d.ts +24 -0
- package/dist/src/dashboard/spa.d.ts.map +1 -0
- package/dist/src/dashboard/spa.js +52 -0
- package/dist/src/dashboard/spa.js.map +1 -0
- package/dist/src/define_config.d.ts +245 -10
- package/dist/src/define_config.d.ts.map +1 -1
- package/dist/src/define_config.js +97 -1
- package/dist/src/define_config.js.map +1 -1
- package/dist/src/diagnostics.d.ts +287 -7
- package/dist/src/diagnostics.d.ts.map +1 -1
- package/dist/src/diagnostics.js +317 -1
- package/dist/src/diagnostics.js.map +1 -1
- package/dist/src/driver.d.ts +139 -5
- package/dist/src/driver.d.ts.map +1 -1
- package/dist/src/drivers/abacate.d.ts +13 -3
- package/dist/src/drivers/abacate.d.ts.map +1 -1
- package/dist/src/drivers/abacate.js +112 -19
- package/dist/src/drivers/abacate.js.map +1 -1
- package/dist/src/drivers/adyen.d.ts +164 -0
- package/dist/src/drivers/adyen.d.ts.map +1 -0
- package/dist/src/drivers/adyen.js +678 -0
- package/dist/src/drivers/adyen.js.map +1 -0
- package/dist/src/drivers/asaas.d.ts +53 -3
- package/dist/src/drivers/asaas.d.ts.map +1 -1
- package/dist/src/drivers/asaas.js +364 -29
- package/dist/src/drivers/asaas.js.map +1 -1
- package/dist/src/drivers/dodo.d.ts +135 -0
- package/dist/src/drivers/dodo.d.ts.map +1 -0
- package/dist/src/drivers/dodo.js +864 -0
- package/dist/src/drivers/dodo.js.map +1 -0
- package/dist/src/drivers/efi.d.ts +98 -0
- package/dist/src/drivers/efi.d.ts.map +1 -0
- package/dist/src/drivers/efi.js +628 -0
- package/dist/src/drivers/efi.js.map +1 -0
- package/dist/src/drivers/infinitepay.d.ts +156 -0
- package/dist/src/drivers/infinitepay.d.ts.map +1 -0
- package/dist/src/drivers/infinitepay.js +321 -0
- package/dist/src/drivers/infinitepay.js.map +1 -0
- package/dist/src/drivers/lemonsqueezy.d.ts +125 -0
- package/dist/src/drivers/lemonsqueezy.d.ts.map +1 -0
- package/dist/src/drivers/lemonsqueezy.js +632 -0
- package/dist/src/drivers/lemonsqueezy.js.map +1 -0
- package/dist/src/drivers/mercadopago.d.ts +97 -0
- package/dist/src/drivers/mercadopago.d.ts.map +1 -0
- package/dist/src/drivers/mercadopago.js +844 -0
- package/dist/src/drivers/mercadopago.js.map +1 -0
- package/dist/src/drivers/mollie.d.ts +125 -0
- package/dist/src/drivers/mollie.d.ts.map +1 -0
- package/dist/src/drivers/mollie.js +812 -0
- package/dist/src/drivers/mollie.js.map +1 -0
- package/dist/src/drivers/paddle.d.ts +148 -0
- package/dist/src/drivers/paddle.d.ts.map +1 -0
- package/dist/src/drivers/paddle.js +738 -0
- package/dist/src/drivers/paddle.js.map +1 -0
- package/dist/src/drivers/pagarme.d.ts +97 -0
- package/dist/src/drivers/pagarme.d.ts.map +1 -0
- package/dist/src/drivers/pagarme.js +829 -0
- package/dist/src/drivers/pagarme.js.map +1 -0
- package/dist/src/drivers/pagbank.d.ts +57 -0
- package/dist/src/drivers/pagbank.d.ts.map +1 -0
- package/dist/src/drivers/pagbank.js +499 -0
- package/dist/src/drivers/pagbank.js.map +1 -0
- package/dist/src/drivers/paypal.d.ts +120 -0
- package/dist/src/drivers/paypal.d.ts.map +1 -0
- package/dist/src/drivers/paypal.js +891 -0
- package/dist/src/drivers/paypal.js.map +1 -0
- package/dist/src/drivers/polar.d.ts +118 -0
- package/dist/src/drivers/polar.d.ts.map +1 -0
- package/dist/src/drivers/polar.js +604 -0
- package/dist/src/drivers/polar.js.map +1 -0
- package/dist/src/drivers/razorpay.d.ts +103 -0
- package/dist/src/drivers/razorpay.d.ts.map +1 -0
- package/dist/src/drivers/razorpay.js +867 -0
- package/dist/src/drivers/razorpay.js.map +1 -0
- package/dist/src/drivers/shared.d.ts +33 -0
- package/dist/src/drivers/shared.d.ts.map +1 -0
- package/dist/src/drivers/shared.js +37 -0
- package/dist/src/drivers/shared.js.map +1 -0
- package/dist/src/drivers/square.d.ts +133 -0
- package/dist/src/drivers/square.d.ts.map +1 -0
- package/dist/src/drivers/square.js +905 -0
- package/dist/src/drivers/square.js.map +1 -0
- package/dist/src/drivers/stripe.d.ts +41 -4
- package/dist/src/drivers/stripe.d.ts.map +1 -1
- package/dist/src/drivers/stripe.js +823 -26
- package/dist/src/drivers/stripe.js.map +1 -1
- package/dist/src/drivers/woovi.d.ts +11 -4
- package/dist/src/drivers/woovi.d.ts.map +1 -1
- package/dist/src/drivers/woovi.js +162 -29
- package/dist/src/drivers/woovi.js.map +1 -1
- package/dist/src/ensure_customer.d.ts +34 -1
- package/dist/src/ensure_customer.d.ts.map +1 -1
- package/dist/src/ensure_customer.js +29 -4
- package/dist/src/ensure_customer.js.map +1 -1
- package/dist/src/hooks/webhook_handlers.d.ts.map +1 -1
- package/dist/src/hooks/webhook_handlers.js.map +1 -1
- package/dist/src/http.d.ts +37 -3
- package/dist/src/http.d.ts.map +1 -1
- package/dist/src/http.js +69 -5
- package/dist/src/http.js.map +1 -1
- package/dist/src/index.d.ts +54 -26
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +29 -12
- package/dist/src/index.js.map +1 -1
- package/dist/src/invoice/drivers/asaas.d.ts.map +1 -1
- package/dist/src/invoice/drivers/asaas.js.map +1 -1
- package/dist/src/invoice/drivers/enotas.d.ts.map +1 -1
- package/dist/src/invoice/drivers/enotas.js.map +1 -1
- package/dist/src/invoice/drivers/focus.d.ts.map +1 -1
- package/dist/src/invoice/drivers/focus.js.map +1 -1
- package/dist/src/invoice/drivers/plugnotas.d.ts.map +1 -1
- package/dist/src/invoice/drivers/plugnotas.js.map +1 -1
- package/dist/src/invoice/drivers/tecnospeed.d.ts.map +1 -1
- package/dist/src/invoice/drivers/tecnospeed.js.map +1 -1
- package/dist/src/invoice/emit_invoice.d.ts.map +1 -1
- package/dist/src/invoice/emit_invoice.js +31 -11
- package/dist/src/invoice/emit_invoice.js.map +1 -1
- package/dist/src/invoice/invoice_manager.d.ts.map +1 -1
- package/dist/src/money.d.ts +23 -5
- package/dist/src/money.d.ts.map +1 -1
- package/dist/src/money.js +72 -7
- package/dist/src/money.js.map +1 -1
- package/dist/src/payments_manager.d.ts +8 -1
- package/dist/src/payments_manager.d.ts.map +1 -1
- package/dist/src/payments_manager.js +168 -6
- package/dist/src/payments_manager.js.map +1 -1
- package/dist/src/services/main.d.ts +38 -0
- package/dist/src/services/main.d.ts.map +1 -1
- package/dist/src/services/main.js +130 -0
- package/dist/src/services/main.js.map +1 -1
- package/dist/src/telescope/index.d.ts +2 -0
- package/dist/src/telescope/index.d.ts.map +1 -1
- package/dist/src/telescope/index.js +1 -0
- package/dist/src/telescope/index.js.map +1 -1
- package/dist/src/telescope/payments_watcher.d.ts +10 -4
- package/dist/src/telescope/payments_watcher.d.ts.map +1 -1
- package/dist/src/telescope/payments_watcher.js +66 -14
- package/dist/src/telescope/payments_watcher.js.map +1 -1
- package/dist/src/telescope/register.d.ts +62 -0
- package/dist/src/telescope/register.d.ts.map +1 -0
- package/dist/src/telescope/register.js +48 -0
- package/dist/src/telescope/register.js.map +1 -0
- package/dist/src/testing/fake_payments.d.ts +62 -0
- package/dist/src/testing/fake_payments.d.ts.map +1 -0
- package/dist/src/testing/fake_payments.js +82 -0
- package/dist/src/testing/fake_payments.js.map +1 -0
- package/dist/src/testing/fake_payments_driver.d.ts +1 -1
- package/dist/src/testing/fake_payments_driver.d.ts.map +1 -1
- package/dist/src/testing/fake_payments_driver.js.map +1 -1
- package/dist/src/testing/in_memory_billing_store.d.ts +151 -2
- package/dist/src/testing/in_memory_billing_store.d.ts.map +1 -1
- package/dist/src/testing/in_memory_billing_store.js +517 -4
- package/dist/src/testing/in_memory_billing_store.js.map +1 -1
- package/dist/src/testing/index.d.ts +3 -2
- package/dist/src/testing/index.d.ts.map +1 -1
- package/dist/src/testing/index.js +1 -0
- package/dist/src/testing/index.js.map +1 -1
- package/dist/src/testing/mutable_clock.d.ts.map +1 -1
- package/dist/src/types.d.ts +161 -5
- package/dist/src/types.d.ts.map +1 -1
- package/dist/src/types.js +30 -1
- package/dist/src/types.js.map +1 -1
- package/dist/src/webhook_handlers.d.ts +96 -2
- package/dist/src/webhook_handlers.d.ts.map +1 -1
- package/dist/src/webhook_handlers.js +91 -10
- package/dist/src/webhook_handlers.js.map +1 -1
- package/dist/src/webhook_security.d.ts +94 -3
- package/dist/src/webhook_security.d.ts.map +1 -1
- package/dist/src/webhook_security.js +173 -9
- package/dist/src/webhook_security.js.map +1 -1
- package/dist/stubs/config/payments.stub +13 -0
- package/dist/stubs/config/payments_client.stub +42 -0
- package/dist/stubs/config/payments_dashboard.stub +38 -0
- package/dist/stubs/database/migrations/create_billing_tables.stub +35 -77
- package/dist/stubs/main.js.map +1 -1
- package/dist/stubs/make/webhook_handler/main.stub +29 -11
- package/package.json +67 -32
|
@@ -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
|