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