@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,812 @@
|
|
|
1
|
+
import { createHmac } from 'node:crypto';
|
|
2
|
+
import { publishPaymentDiagnostics, publishRefundDiagnostics, publishSubscriptionDiagnostics, } from '../diagnostics.js';
|
|
3
|
+
import { headerValue, httpRequest, isNotFound } from '../http.js';
|
|
4
|
+
import { emitInvoiceIfRequested } from '../invoice/emit_invoice.js';
|
|
5
|
+
import { formatDecimal, fromDecimal } from '../money.js';
|
|
6
|
+
import { safeCompare } from '../webhook_security.js';
|
|
7
|
+
import { requireCredential, requireCurrency } from './shared.js';
|
|
8
|
+
/**
|
|
9
|
+
* Canonical method **category** → the Mollie method ids in it.
|
|
10
|
+
*
|
|
11
|
+
* Mollie's `method` parameter takes a single id *or an array of ids*, and an array is
|
|
12
|
+
* exactly what a category is: `bank_transfer` is not one brand, it is iDEAL in the
|
|
13
|
+
* Netherlands, Bancontact in Belgium, Multibanco in Portugal and eleven others. Sending
|
|
14
|
+
* the whole set restricts the hosted page to that category and lets Mollie show the
|
|
15
|
+
* buyer the ones their country and your profile actually enable.
|
|
16
|
+
*
|
|
17
|
+
* To pin one brand — "iDEAL, nothing else" — pass `metadata.mollieMethod` with the exact
|
|
18
|
+
* Mollie id; that is the gateway's own field, which is where a brand belongs.
|
|
19
|
+
*/
|
|
20
|
+
const MOLLIE_METHOD_CATEGORIES = {
|
|
21
|
+
credit_card: ['creditcard'],
|
|
22
|
+
// Push-from-your-bank. Mollie's own docs group these as its "bank-based" methods.
|
|
23
|
+
bank_transfer: [
|
|
24
|
+
'ideal',
|
|
25
|
+
'bancontact',
|
|
26
|
+
'banktransfer',
|
|
27
|
+
'belfius',
|
|
28
|
+
'bizum',
|
|
29
|
+
'blik',
|
|
30
|
+
'eps',
|
|
31
|
+
'kbc',
|
|
32
|
+
'mbway',
|
|
33
|
+
'mobilepay',
|
|
34
|
+
'multibanco',
|
|
35
|
+
'mybank',
|
|
36
|
+
'paybybank',
|
|
37
|
+
'przelewy24',
|
|
38
|
+
'satispay',
|
|
39
|
+
'swish',
|
|
40
|
+
'trustly',
|
|
41
|
+
'twint',
|
|
42
|
+
'vipps',
|
|
43
|
+
'bancomatpay',
|
|
44
|
+
],
|
|
45
|
+
// Pull-from-your-account mandates: SEPA Direct Debit and its UK equivalent, Bacs.
|
|
46
|
+
bank_debit: ['directdebit', 'bacs'],
|
|
47
|
+
wallet: ['paypal', 'applepay', 'googlepay'],
|
|
48
|
+
bnpl: ['klarna', 'in3', 'riverty', 'billie', 'billink', 'alma'],
|
|
49
|
+
// Stored-value paper and plastic: meal/eco vouchers, gift cards, paysafecard.
|
|
50
|
+
voucher: ['voucher', 'giftcard', 'paysafecard'],
|
|
51
|
+
};
|
|
52
|
+
/**
|
|
53
|
+
* Mollie driver — European gateway (api.mollie.com/v2), Bearer API key, multi-currency.
|
|
54
|
+
*
|
|
55
|
+
* Two things about Mollie shape this driver more than anything else:
|
|
56
|
+
*
|
|
57
|
+
* 1. **Money is a decimal string** (`{ "currency": "EUR", "value": "10.00" }`). The whole
|
|
58
|
+
* package works in the currency's smallest unit, so the conversion happens once on the
|
|
59
|
+
* way out (`#toMollieAmount`) and once on the way in (`#fromMollieAmount`).
|
|
60
|
+
* 2. **The classic webhook is a bare id** — `id=tr_xxx`, form-encoded, unsigned. Mollie
|
|
61
|
+
* deliberately leaves the status out so a forged call cannot tell you a payment was
|
|
62
|
+
* paid; you learn what happened, and that the call is genuine, by fetching the payment
|
|
63
|
+
* with your API key. `parseWebhook` is async here for exactly that reason.
|
|
64
|
+
*/
|
|
65
|
+
export class MollieDriver {
|
|
66
|
+
provider = 'mollie';
|
|
67
|
+
/**
|
|
68
|
+
* What `charge()` can genuinely ask Mollie for, now that the contract names **categories**
|
|
69
|
+
* instead of brands. Mollie's catalogue is almost entirely local European methods and
|
|
70
|
+
* none of them had a name here, so every one of iDEAL, Bancontact, SEPA Direct Debit,
|
|
71
|
+
* Klarna, PayPal, Apple Pay, EPS, Przelewy24, BLIK, TWINT, MB WAY, Multibanco, Trustly,
|
|
72
|
+
* paysafecard and the vouchers was unroutable. Each now falls into a category, and the
|
|
73
|
+
* category goes out as Mollie's own `method` array (see {@link MollieDriver.#mapMethod}).
|
|
74
|
+
*
|
|
75
|
+
* `debit_card` is still absent, and re-checking did not change that: Mollie folds debit
|
|
76
|
+
* cards (Maestro, V PAY) into the single `creditcard` method id and exposes no separate
|
|
77
|
+
* debit id, so a `debit_card` route could only be a `creditcard` charge wearing another
|
|
78
|
+
* name. `pix` and `boleto` are not European methods and Mollie has neither.
|
|
79
|
+
*/
|
|
80
|
+
supportedMethods = [
|
|
81
|
+
'credit_card',
|
|
82
|
+
'bank_transfer',
|
|
83
|
+
'bank_debit',
|
|
84
|
+
'wallet',
|
|
85
|
+
'bnpl',
|
|
86
|
+
'voucher',
|
|
87
|
+
'undefined',
|
|
88
|
+
];
|
|
89
|
+
/** No invoices: Mollie's Invoices API returns Mollie's own invoices to *you*, not yours. */
|
|
90
|
+
capabilities = { refunds: true, invoices: false, subscriptions: true };
|
|
91
|
+
#baseUrl = 'https://api.mollie.com/v2';
|
|
92
|
+
#apiKey;
|
|
93
|
+
#currency;
|
|
94
|
+
#webhookSecret;
|
|
95
|
+
#invoiceCtx;
|
|
96
|
+
/**
|
|
97
|
+
* Subscription id → customer id. Every Mollie subscription endpoint is nested under a
|
|
98
|
+
* customer, but the driver contract passes only the subscription id; this remembers the
|
|
99
|
+
* pairing for subscriptions this process created, so the common path costs no lookup.
|
|
100
|
+
*/
|
|
101
|
+
#subscriptionCustomers = new Map();
|
|
102
|
+
constructor(ctx, config) {
|
|
103
|
+
this.#invoiceCtx = ctx;
|
|
104
|
+
this.#apiKey = requireCredential({
|
|
105
|
+
driver: 'mollie',
|
|
106
|
+
option: 'apiKey',
|
|
107
|
+
env: 'MOLLIE_API_KEY',
|
|
108
|
+
value: config.apiKey,
|
|
109
|
+
});
|
|
110
|
+
this.#currency = requireCurrency('mollie', config.currency);
|
|
111
|
+
this.#webhookSecret = config.webhookSecret ?? process.env.MOLLIE_WEBHOOK_SECRET;
|
|
112
|
+
}
|
|
113
|
+
// ── Customers ────────────────────────────────────────────────────────────────────────
|
|
114
|
+
async createCustomer(input) {
|
|
115
|
+
const data = await this.#request('/customers', {
|
|
116
|
+
method: 'POST',
|
|
117
|
+
body: {
|
|
118
|
+
...(input.name !== undefined ? { name: input.name } : {}),
|
|
119
|
+
...(input.email !== undefined ? { email: input.email } : {}),
|
|
120
|
+
...this.#customerMetadata(input),
|
|
121
|
+
},
|
|
122
|
+
...this.#idempotency(input.idempotencyKey),
|
|
123
|
+
});
|
|
124
|
+
return this.#mapCustomer(data);
|
|
125
|
+
}
|
|
126
|
+
async findCustomer(customerId) {
|
|
127
|
+
try {
|
|
128
|
+
return this.#mapCustomer(await this.#request(`/customers/${customerId}`));
|
|
129
|
+
}
|
|
130
|
+
catch (error) {
|
|
131
|
+
if (isNotFound(error))
|
|
132
|
+
return null;
|
|
133
|
+
throw error;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
async updateCustomer(customerId, input) {
|
|
137
|
+
const data = await this.#request(`/customers/${customerId}`, {
|
|
138
|
+
method: 'PATCH',
|
|
139
|
+
body: {
|
|
140
|
+
...(input.name !== undefined ? { name: input.name } : {}),
|
|
141
|
+
...(input.email !== undefined ? { email: input.email } : {}),
|
|
142
|
+
...this.#customerMetadata(input),
|
|
143
|
+
},
|
|
144
|
+
});
|
|
145
|
+
return this.#mapCustomer(data);
|
|
146
|
+
}
|
|
147
|
+
// ── Payments ─────────────────────────────────────────────────────────────────────────
|
|
148
|
+
async charge(input) {
|
|
149
|
+
if (input.split !== undefined && input.split.length > 0) {
|
|
150
|
+
throw new Error('[payments] Mollie does not take a percent/fixed wallet split on a payment. ' +
|
|
151
|
+
'Its equivalent (Mollie Connect routes) splits to organization ids with absolute ' +
|
|
152
|
+
'amounts only, which `split` cannot express — create the routes through the Mollie API.');
|
|
153
|
+
}
|
|
154
|
+
// A stored card at Mollie is a *mandate*, and charging one is `sequenceType: recurring`.
|
|
155
|
+
const mandateId = input.card?.token ?? input.paymentMethodId;
|
|
156
|
+
const recurring = mandateId !== undefined;
|
|
157
|
+
const transport = this.#transportOptions(input.metadata);
|
|
158
|
+
// Mollie requires `redirectUrl` on every payment the shopper completes themselves; a
|
|
159
|
+
// recurring charge against a mandate is the one case with nobody to redirect.
|
|
160
|
+
if (!recurring && transport.redirectUrl === undefined) {
|
|
161
|
+
throw new Error('[payments] Mollie requires a redirect URL on a customer-facing payment. ' +
|
|
162
|
+
'Pass `metadata.redirectUrl` on the charge, or use `createCheckout({ successUrl })`.');
|
|
163
|
+
}
|
|
164
|
+
if (recurring && input.customerId === undefined) {
|
|
165
|
+
throw new Error('[payments] Mollie needs the customer that owns the mandate. Pass `customerId` ' +
|
|
166
|
+
'alongside the mandate id when charging a stored card.');
|
|
167
|
+
}
|
|
168
|
+
const method = this.#mapMethod(input.method, input.metadata);
|
|
169
|
+
const body = {
|
|
170
|
+
amount: this.#toMollieAmount(input.amount, input.currency),
|
|
171
|
+
// Mollie makes `description` mandatory — it is what the payer sees on their statement.
|
|
172
|
+
description: input.description ?? 'Payment',
|
|
173
|
+
...(transport.redirectUrl !== undefined ? { redirectUrl: transport.redirectUrl } : {}),
|
|
174
|
+
...(transport.cancelUrl !== undefined ? { cancelUrl: transport.cancelUrl } : {}),
|
|
175
|
+
...(transport.webhookUrl !== undefined ? { webhookUrl: transport.webhookUrl } : {}),
|
|
176
|
+
...(method !== undefined ? { method } : {}),
|
|
177
|
+
...(input.customerId !== undefined ? { customerId: input.customerId } : {}),
|
|
178
|
+
...(mandateId !== undefined ? { mandateId, sequenceType: 'recurring' } : {}),
|
|
179
|
+
...this.#metadataBody(transport.rest, input.externalReference),
|
|
180
|
+
};
|
|
181
|
+
const data = await this.#request('/payments', {
|
|
182
|
+
method: 'POST',
|
|
183
|
+
body,
|
|
184
|
+
...this.#idempotency(input.idempotencyKey),
|
|
185
|
+
});
|
|
186
|
+
const payment = this.#mapPayment(data);
|
|
187
|
+
await emitInvoiceIfRequested(this.#invoiceCtx, input, payment, this);
|
|
188
|
+
publishPaymentDiagnostics(payment);
|
|
189
|
+
return payment;
|
|
190
|
+
}
|
|
191
|
+
async findPayment(gatewayId) {
|
|
192
|
+
try {
|
|
193
|
+
return this.#mapPayment(await this.#request(`/payments/${gatewayId}`));
|
|
194
|
+
}
|
|
195
|
+
catch (error) {
|
|
196
|
+
if (isNotFound(error))
|
|
197
|
+
return null;
|
|
198
|
+
throw error;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
/**
|
|
202
|
+
* `options.idempotencyKey` goes out as Mollie's `Idempotency-Key` header — the only
|
|
203
|
+
* thing Mollie deduplicates on, and the difference between a retried refund and a second
|
|
204
|
+
* one. Mollie keeps a key for one hour; after that the same key refunds again.
|
|
205
|
+
*/
|
|
206
|
+
async refund(paymentGatewayId, amount, options) {
|
|
207
|
+
// Mollie makes `amount` mandatory on a refund and the amount must carry the *payment's*
|
|
208
|
+
// currency, which a charge may have overridden — so read it off the payment rather than
|
|
209
|
+
// assuming the driver's configured one.
|
|
210
|
+
const payment = await this.#request(`/payments/${paymentGatewayId}`);
|
|
211
|
+
const value = amount !== undefined
|
|
212
|
+
? {
|
|
213
|
+
currency: payment.amount.currency,
|
|
214
|
+
value: formatDecimal(amount, payment.amount.currency),
|
|
215
|
+
}
|
|
216
|
+
: payment.amount;
|
|
217
|
+
const data = await this.#request(`/payments/${paymentGatewayId}/refunds`, { method: 'POST', body: { amount: value }, ...this.#idempotency(options?.idempotencyKey) });
|
|
218
|
+
const refund = {
|
|
219
|
+
id: data.id,
|
|
220
|
+
gatewayId: data.id,
|
|
221
|
+
provider: this.provider,
|
|
222
|
+
amount: this.#fromMollieAmount(data.amount),
|
|
223
|
+
status: data.status === 'refunded'
|
|
224
|
+
? 'succeeded'
|
|
225
|
+
: data.status === 'failed' || data.status === 'canceled'
|
|
226
|
+
? 'failed'
|
|
227
|
+
: 'pending',
|
|
228
|
+
createdAt: data.createdAt,
|
|
229
|
+
};
|
|
230
|
+
publishRefundDiagnostics(refund);
|
|
231
|
+
return refund;
|
|
232
|
+
}
|
|
233
|
+
// ── Checkout ─────────────────────────────────────────────────────────────────────────
|
|
234
|
+
async createCheckout(input) {
|
|
235
|
+
if (input.planId !== undefined || input.trialDays !== undefined) {
|
|
236
|
+
throw new Error('[payments] Mollie has no subscription checkout: a subscription needs a mandate, ' +
|
|
237
|
+
'which the shopper grants by completing a first payment. Check out the first ' +
|
|
238
|
+
'payment, then call `createSubscription` once the mandate is valid.');
|
|
239
|
+
}
|
|
240
|
+
const transport = this.#transportOptions(input.metadata);
|
|
241
|
+
const data = await this.#request('/payments', {
|
|
242
|
+
method: 'POST',
|
|
243
|
+
body: {
|
|
244
|
+
amount: this.#toMollieAmount(input.amount, input.currency),
|
|
245
|
+
description: input.description ?? 'Checkout',
|
|
246
|
+
redirectUrl: input.successUrl,
|
|
247
|
+
...(input.cancelUrl !== undefined ? { cancelUrl: input.cancelUrl } : {}),
|
|
248
|
+
...(transport.webhookUrl !== undefined ? { webhookUrl: transport.webhookUrl } : {}),
|
|
249
|
+
...(input.customerId !== undefined ? { customerId: input.customerId } : {}),
|
|
250
|
+
...this.#metadataBody(transport.rest, input.externalReference),
|
|
251
|
+
},
|
|
252
|
+
...this.#idempotency(input.idempotencyKey),
|
|
253
|
+
});
|
|
254
|
+
const url = data._links?.checkout?.href;
|
|
255
|
+
if (url === undefined) {
|
|
256
|
+
// An empty redirect URL fails at the browser, long after the caller could react.
|
|
257
|
+
throw new Error(`[payments] Mollie returned payment ${data.id} without a checkout link. That happens when the payment cannot be paid by the shopper (e.g. a recurring sequence) — use \`charge()\` for those.`);
|
|
258
|
+
}
|
|
259
|
+
return {
|
|
260
|
+
id: data.id,
|
|
261
|
+
gatewayId: data.id,
|
|
262
|
+
provider: this.provider,
|
|
263
|
+
url,
|
|
264
|
+
status: data.status === 'paid' ? 'complete' : data.status === 'expired' ? 'expired' : 'open',
|
|
265
|
+
amount: this.#fromMollieAmount(data.amount),
|
|
266
|
+
...(data.customerId !== undefined ? { customerId: data.customerId } : {}),
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
// ── Subscriptions ────────────────────────────────────────────────────────────────────
|
|
270
|
+
async createSubscription(input) {
|
|
271
|
+
if (input.amount === undefined) {
|
|
272
|
+
throw new Error('[payments] Mollie requires an `amount` on every subscription.');
|
|
273
|
+
}
|
|
274
|
+
if (input.trialDays !== undefined) {
|
|
275
|
+
throw new Error('[payments] Mollie has no trial period on a subscription — it would have to be ' +
|
|
276
|
+
'faked as a later first charge, and nothing at the gateway would call it a trial. ' +
|
|
277
|
+
'Pass `startDate` for the first charge date instead.');
|
|
278
|
+
}
|
|
279
|
+
const transport = this.#transportOptions(input.metadata);
|
|
280
|
+
const body = {
|
|
281
|
+
amount: this.#toMollieAmount(input.amount, undefined),
|
|
282
|
+
interval: this.#mapCycle(input.cycle),
|
|
283
|
+
// Mollie requires a description and shows it on the payer's statement; it must be
|
|
284
|
+
// unique among a customer's active subscriptions, so the plan id is a good default.
|
|
285
|
+
description: input.description ?? input.planId,
|
|
286
|
+
...(input.startDate !== undefined ? { startDate: input.startDate } : {}),
|
|
287
|
+
...(input.card?.token !== undefined ? { mandateId: input.card.token } : {}),
|
|
288
|
+
...(transport.webhookUrl !== undefined ? { webhookUrl: transport.webhookUrl } : {}),
|
|
289
|
+
// Mollie has no plan resource, so the plan id survives only as metadata — and
|
|
290
|
+
// `#mapSubscription` reads it back out of there.
|
|
291
|
+
...this.#metadataBody({ ...transport.rest, planId: input.planId }, input.externalReference),
|
|
292
|
+
};
|
|
293
|
+
const data = await this.#request(`/customers/${input.customerId}/subscriptions`, { method: 'POST', body, ...this.#idempotency(input.idempotencyKey) });
|
|
294
|
+
const subscription = this.#mapSubscription(data);
|
|
295
|
+
publishSubscriptionDiagnostics(subscription, 'subscription.created');
|
|
296
|
+
return subscription;
|
|
297
|
+
}
|
|
298
|
+
async cancelSubscription(subscriptionGatewayId, _options) {
|
|
299
|
+
// Mollie cancels the schedule outright; there is no period-end flag to honor.
|
|
300
|
+
const { customerId, subscriptionId } = await this.#resolveSubscription(subscriptionGatewayId);
|
|
301
|
+
const data = await this.#request(`/customers/${customerId}/subscriptions/${subscriptionId}`, { method: 'DELETE' });
|
|
302
|
+
const subscription = this.#mapSubscription(data);
|
|
303
|
+
publishSubscriptionDiagnostics(subscription, 'subscription.canceled');
|
|
304
|
+
return subscription;
|
|
305
|
+
}
|
|
306
|
+
/**
|
|
307
|
+
* `input.idempotencyKey` is REFUSED, not ignored. Mollie's own words: "All `POST`
|
|
308
|
+
* endpoints accept idempotency keys. Sending idempotency keys for `GET`, `PATCH`, or
|
|
309
|
+
* `DELETE` requests is not necessary since these API requests are repeatable by nature."
|
|
310
|
+
* Updating a subscription is a `PATCH`, so there is no key to send and accepting one
|
|
311
|
+
* would promise a deduplication Mollie never performs.
|
|
312
|
+
*/
|
|
313
|
+
async updateSubscription(subscriptionGatewayId, input) {
|
|
314
|
+
if (input.idempotencyKey !== undefined) {
|
|
315
|
+
throw new Error('[payments] Mollie accepts `Idempotency-Key` on POST requests only, and updating a ' +
|
|
316
|
+
'subscription is a PATCH — so `idempotencyKey` cannot be honoured on ' +
|
|
317
|
+
'updateSubscription(). The PATCH is repeatable by nature (it sets fields to the ' +
|
|
318
|
+
'values you pass), so drop the key rather than relying on one Mollie ignores.');
|
|
319
|
+
}
|
|
320
|
+
const { customerId, subscriptionId } = await this.#resolveSubscription(subscriptionGatewayId);
|
|
321
|
+
const data = await this.#request(`/customers/${customerId}/subscriptions/${subscriptionId}`, {
|
|
322
|
+
method: 'PATCH',
|
|
323
|
+
body: {
|
|
324
|
+
...(input.amount !== undefined
|
|
325
|
+
? { amount: this.#toMollieAmount(input.amount, undefined) }
|
|
326
|
+
: {}),
|
|
327
|
+
...(input.description !== undefined ? { description: input.description } : {}),
|
|
328
|
+
...(input.metadata !== undefined ? { metadata: input.metadata } : {}),
|
|
329
|
+
},
|
|
330
|
+
});
|
|
331
|
+
return this.#mapSubscription(data);
|
|
332
|
+
}
|
|
333
|
+
async findSubscription(gatewayId) {
|
|
334
|
+
try {
|
|
335
|
+
const { customerId, subscriptionId } = await this.#resolveSubscription(gatewayId);
|
|
336
|
+
return this.#mapSubscription(await this.#request(`/customers/${customerId}/subscriptions/${subscriptionId}`));
|
|
337
|
+
}
|
|
338
|
+
catch (error) {
|
|
339
|
+
if (isNotFound(error))
|
|
340
|
+
return null;
|
|
341
|
+
throw error;
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
// ── Invoices ─────────────────────────────────────────────────────────────────────────
|
|
345
|
+
async listInvoices(_customerId) {
|
|
346
|
+
throw new Error('[payments] Mollie has no customer invoices to list: its Invoices API returns the ' +
|
|
347
|
+
'monthly invoices Mollie issues to *you* for its fees, not documents you issue to ' +
|
|
348
|
+
'your customers. Configure an `invoice` provider and pass `invoice: true` on the charge.');
|
|
349
|
+
}
|
|
350
|
+
// ── Webhooks ─────────────────────────────────────────────────────────────────────────
|
|
351
|
+
/**
|
|
352
|
+
* Mollie's classic webhook carries one thing: `id=tr_xxx`, form-encoded and unsigned.
|
|
353
|
+
* That is deliberate — because the body never says "paid", a forged call cannot make you
|
|
354
|
+
* think a payment succeeded. **Authenticity comes from the fetch, not from the request**:
|
|
355
|
+
* the driver reads the payment back with your API key, and what that authenticated call
|
|
356
|
+
* returns is the event. Nothing here trusts a word of the request body beyond the id.
|
|
357
|
+
*
|
|
358
|
+
* Which is why this is async. A synchronous parse could only report "payment tr_x
|
|
359
|
+
* changed", so the mounted route ledgered a row and never marked the payment paid — the
|
|
360
|
+
* money arriving and the app never finding out.
|
|
361
|
+
*
|
|
362
|
+
* A fetch failure propagates on purpose: the route answers 400 and Mollie retries, which
|
|
363
|
+
* is the right outcome for a payment nobody could confirm.
|
|
364
|
+
*
|
|
365
|
+
* When `webhookSecret` is configured the driver is talking to Mollie's *next-gen*
|
|
366
|
+
* webhooks instead, which send a JSON event and sign it with `X-Mollie-Signature`
|
|
367
|
+
* (`sha256=<hex hmac of the raw body>`) — then verification is mandatory, fail-closed.
|
|
368
|
+
* Those still get fetched: a signature proves who sent the event, not what the payment
|
|
369
|
+
* is worth, and the built-in billing sync needs the amount.
|
|
370
|
+
*/
|
|
371
|
+
/**
|
|
372
|
+
* Whether a delivery to `POST /payments/webhook/:provider` can be authenticated.
|
|
373
|
+
*
|
|
374
|
+
* Next-gen webhooks are signed, so a configured secret is `'configured'`. Without one the
|
|
375
|
+
* driver is on the CLASSIC flow, and that flow is `'unsupported'` rather than
|
|
376
|
+
* `'unconfigured'` — the distinction matters, because `'unconfigured'` refuses to boot.
|
|
377
|
+
*
|
|
378
|
+
* There is no credential to configure on the classic flow and nothing insecure about not
|
|
379
|
+
* having one: the request carries only `id=tr_xxx`, and the driver authenticates by
|
|
380
|
+
* reading that payment back with your API key (see `parseWebhook` below). Reporting it as
|
|
381
|
+
* a missing credential would make the supported, safe configuration fail at boot and push
|
|
382
|
+
* apps into `allowUnverifiedWebhooks`, which would then also silence a genuinely missing
|
|
383
|
+
* next-gen secret.
|
|
384
|
+
*/
|
|
385
|
+
get webhookVerification() {
|
|
386
|
+
return this.#webhookSecret !== undefined ? 'configured' : 'unsupported';
|
|
387
|
+
}
|
|
388
|
+
async parseWebhook(rawBody, headers) {
|
|
389
|
+
if (this.#webhookSecret !== undefined) {
|
|
390
|
+
this.#verifySignature(rawBody, headerValue(headers, 'x-mollie-signature'));
|
|
391
|
+
}
|
|
392
|
+
const envelope = this.#parseEventPayload(rawBody);
|
|
393
|
+
const entityId = envelope !== null ? envelope.entityId : this.#classicId(rawBody);
|
|
394
|
+
if (entityId === undefined || entityId === '') {
|
|
395
|
+
throw new Error('[payments] Mollie webhook body carried no resource id.');
|
|
396
|
+
}
|
|
397
|
+
if (entityId.startsWith('chb_')) {
|
|
398
|
+
return this.#chargebackEvent(envelope, entityId);
|
|
399
|
+
}
|
|
400
|
+
if (!entityId.startsWith('tr_')) {
|
|
401
|
+
// A next-gen event about something that is not a payment (a payment link, a sales
|
|
402
|
+
// invoice). There is nothing to map onto the payment shape, so report what the
|
|
403
|
+
// signed envelope said and leave it to an app handler.
|
|
404
|
+
return {
|
|
405
|
+
id: envelope?.id ?? `mollie:${entityId}`,
|
|
406
|
+
provider: this.provider,
|
|
407
|
+
type: this.#mapEventType(envelope?.type),
|
|
408
|
+
...(envelope?.createdAt !== undefined ? { createdAt: envelope.createdAt } : {}),
|
|
409
|
+
data: { gatewayId: entityId },
|
|
410
|
+
raw: (envelope ?? { id: entityId }),
|
|
411
|
+
};
|
|
412
|
+
}
|
|
413
|
+
const data = await this.#request(`/payments/${entityId}`);
|
|
414
|
+
const payment = this.#mapPayment(data);
|
|
415
|
+
return {
|
|
416
|
+
// Stable per *transition*, so a redelivery of the same status dedupes in the ledger
|
|
417
|
+
// while the next status still gets through. A next-gen event has a real id of its own.
|
|
418
|
+
//
|
|
419
|
+
// Mollie's own `status` stays `paid` through a refund AND through a chargeback, so
|
|
420
|
+
// the derived id needs the mapped outcome too — without it the chargeback's event id
|
|
421
|
+
// is byte-identical to the earlier `payment.succeeded` one and the ledger discards
|
|
422
|
+
// the webhook that takes the money away as a replay.
|
|
423
|
+
id: envelope?.id ?? `mollie:${data.id}:${this.#transition(data.status, payment.status)}`,
|
|
424
|
+
provider: this.provider,
|
|
425
|
+
type: payment.status === 'disputed'
|
|
426
|
+
? 'payment.disputed'
|
|
427
|
+
: payment.status === 'paid'
|
|
428
|
+
? 'payment.succeeded'
|
|
429
|
+
: payment.status === 'refunded'
|
|
430
|
+
? 'payment.refunded'
|
|
431
|
+
: payment.status === 'failed' || payment.status === 'canceled'
|
|
432
|
+
? 'payment.failed'
|
|
433
|
+
: 'payment.updated',
|
|
434
|
+
createdAt: data.createdAt,
|
|
435
|
+
data: {
|
|
436
|
+
gatewayId: payment.gatewayId,
|
|
437
|
+
amount: payment.amount.amount,
|
|
438
|
+
currency: payment.amount.currency,
|
|
439
|
+
...(payment.customerId !== undefined ? { customerId: payment.customerId } : {}),
|
|
440
|
+
...(payment.subscriptionId !== undefined ? { subscriptionId: payment.subscriptionId } : {}),
|
|
441
|
+
...(typeof data.metadata?.externalReference === 'string'
|
|
442
|
+
? { externalReference: data.metadata.externalReference }
|
|
443
|
+
: {}),
|
|
444
|
+
...(data.metadata != null ? { metadata: data.metadata } : {}),
|
|
445
|
+
},
|
|
446
|
+
// The request body carried only an id; the fetched payment is the gateway payload
|
|
447
|
+
// this event was actually normalized from.
|
|
448
|
+
raw: data,
|
|
449
|
+
};
|
|
450
|
+
}
|
|
451
|
+
/**
|
|
452
|
+
* A next-gen `chargeback.*` event: the one webhook that takes revenue away, and the one
|
|
453
|
+
* that gives it back.
|
|
454
|
+
*
|
|
455
|
+
* Mollie has exactly two of them — `chargeback.received` ("a chargeback has been received
|
|
456
|
+
* for a payment") and `chargeback.reversed` ("a previously received chargeback has been
|
|
457
|
+
* reversed") — and **no pre-dispute vocabulary at all**: no fraud alert, no retrieval
|
|
458
|
+
* request, no inquiry, and no `payment.dispute_warning` for this driver to emit. Mollie's
|
|
459
|
+
* own words are that the money "will be reclaimed and deducted from your Mollie balance",
|
|
460
|
+
* so the first event already is the withdrawal.
|
|
461
|
+
*
|
|
462
|
+
* The chargeback object carries no response deadline either — its whole field list is
|
|
463
|
+
* `id`, `amount`, `settlementAmount`, `reason`, `paymentId`, `settlementId`, `createdAt`,
|
|
464
|
+
* `reversedAt` — so no `actionableUntil` is emitted rather than one being invented.
|
|
465
|
+
*
|
|
466
|
+
* The reversal is read from `reversedAt` as well as from the event name: the payload is a
|
|
467
|
+
* snapshot of the entity, so a `chargeback.received` redelivered after the reversal
|
|
468
|
+
* carries the timestamp too, and taking the money back off the row twice would be worse
|
|
469
|
+
* than reading it from either place.
|
|
470
|
+
*
|
|
471
|
+
* The chargeback entity has to come from the event body. Mollie's only read endpoint is
|
|
472
|
+
* `GET /payments/{paymentId}/chargebacks/{id}` — there is no lookup by chargeback id —
|
|
473
|
+
* so a webhook configured with the "simple payload" (an `entityId` and nothing else)
|
|
474
|
+
* carries a chargeback that literally cannot be resolved. That throws rather than
|
|
475
|
+
* degrading to an inert event: the route answers 400, Mollie retries, and the message
|
|
476
|
+
* says which setting to change. A silent pass-through here is exactly the shape of bug
|
|
477
|
+
* `payment.disputed` exists to remove.
|
|
478
|
+
*
|
|
479
|
+
* Note this is the *next-gen* path only. On the classic webhook a chargeback arrives as
|
|
480
|
+
* an ordinary `tr_…` notification and is caught by `amountChargedBack` on the fetched
|
|
481
|
+
* payment — see {@link MollieDriver.#mapPayment}.
|
|
482
|
+
*/
|
|
483
|
+
#chargebackEvent(envelope, entityId) {
|
|
484
|
+
const entity = envelope?._embedded?.entity;
|
|
485
|
+
if (entity?.paymentId === undefined || entity.amount === undefined) {
|
|
486
|
+
throw new Error(`[payments] Mollie sent chargeback ${entityId} with no embedded entity, and a chargeback cannot be read back on its own — \`GET /payments/{paymentId}/chargebacks/{id}\` needs the payment id Mollie did not send. Configure this webhook to deliver the full snapshot payload instead of the id-only one.`);
|
|
487
|
+
}
|
|
488
|
+
const amount = this.#fromMollieAmount(entity.amount);
|
|
489
|
+
const reversed = envelope?.type === 'chargeback.reversed' || typeof entity.reversedAt === 'string';
|
|
490
|
+
return {
|
|
491
|
+
id: envelope?.id ?? `mollie:${entityId}:${envelope?.type ?? 'chargeback'}`,
|
|
492
|
+
provider: this.provider,
|
|
493
|
+
// A reversal is the dispute resolved in your favour and the money back on the
|
|
494
|
+
// balance, so it closes the dispute as `won` — the processor is what moves the row
|
|
495
|
+
// off `disputed`, and reporting the reversal as a plain update left it stuck there
|
|
496
|
+
// with the revenue written off.
|
|
497
|
+
type: reversed ? 'payment.dispute_closed' : 'payment.disputed',
|
|
498
|
+
...(envelope?.createdAt !== undefined ? { createdAt: envelope.createdAt } : {}),
|
|
499
|
+
// Keyed by the PAYMENT, not the chargeback: the payment is the row whose status has
|
|
500
|
+
// to move to `disputed`.
|
|
501
|
+
data: {
|
|
502
|
+
gatewayId: entity.paymentId,
|
|
503
|
+
amount: amount.amount,
|
|
504
|
+
currency: amount.currency,
|
|
505
|
+
disputeId: entity.id ?? entityId,
|
|
506
|
+
...(reversed ? { outcome: 'won' } : {}),
|
|
507
|
+
// Only ever set on a SEPA Direct Debit chargeback — Mollie's reference says the
|
|
508
|
+
// card schemes send it no reason at all.
|
|
509
|
+
...(entity.reason?.code !== undefined ? { reason: entity.reason.code } : {}),
|
|
510
|
+
},
|
|
511
|
+
raw: (envelope ?? { id: entityId }),
|
|
512
|
+
};
|
|
513
|
+
}
|
|
514
|
+
// ── Mappers ──────────────────────────────────────────────────────────────────────────
|
|
515
|
+
#mapCustomer(data) {
|
|
516
|
+
const taxId = data.metadata?.taxId;
|
|
517
|
+
return {
|
|
518
|
+
id: data.id,
|
|
519
|
+
...(data.name != null ? { name: data.name } : {}),
|
|
520
|
+
...(data.email != null ? { email: data.email } : {}),
|
|
521
|
+
...(typeof taxId === 'string' ? { taxId } : {}),
|
|
522
|
+
...(data.metadata != null ? { metadata: data.metadata } : {}),
|
|
523
|
+
};
|
|
524
|
+
}
|
|
525
|
+
#mapPayment(data) {
|
|
526
|
+
const amount = this.#fromMollieAmount(data.amount);
|
|
527
|
+
const refunded = data.amountRefunded !== undefined &&
|
|
528
|
+
this.#fromMollieAmount(data.amountRefunded).amount >= amount.amount;
|
|
529
|
+
// A chargeback leaves Mollie's own `status` at `paid` — `amountChargedBack` is the only
|
|
530
|
+
// field that says the bank pulled the money back, and it outranks everything below it.
|
|
531
|
+
const chargedBack = data.amountChargedBack !== undefined &&
|
|
532
|
+
this.#fromMollieAmount(data.amountChargedBack).amount > 0;
|
|
533
|
+
const statusMap = {
|
|
534
|
+
open: 'pending',
|
|
535
|
+
pending: 'pending',
|
|
536
|
+
// Funds held, nothing captured — a real Mollie state for pay-later methods and for
|
|
537
|
+
// manual-capture cards. It used to collapse into `pending`, which understated it.
|
|
538
|
+
authorized: 'authorized',
|
|
539
|
+
paid: 'paid',
|
|
540
|
+
canceled: 'canceled',
|
|
541
|
+
// The payer never completed it and now cannot — the same outcome as `failed`, and
|
|
542
|
+
// `canceled` would wrongly suggest somebody canceled it.
|
|
543
|
+
expired: 'failed',
|
|
544
|
+
failed: 'failed',
|
|
545
|
+
};
|
|
546
|
+
const result = {
|
|
547
|
+
id: data.id,
|
|
548
|
+
gatewayId: data.id,
|
|
549
|
+
provider: this.provider,
|
|
550
|
+
amount,
|
|
551
|
+
status: chargedBack
|
|
552
|
+
? 'disputed'
|
|
553
|
+
: refunded
|
|
554
|
+
? 'refunded'
|
|
555
|
+
: (statusMap[data.status] ?? 'pending'),
|
|
556
|
+
payload: data,
|
|
557
|
+
createdAt: data.createdAt,
|
|
558
|
+
};
|
|
559
|
+
if (data.customerId !== undefined)
|
|
560
|
+
result.customerId = data.customerId;
|
|
561
|
+
if (data.subscriptionId !== undefined)
|
|
562
|
+
result.subscriptionId = data.subscriptionId;
|
|
563
|
+
if (data.paidAt !== undefined)
|
|
564
|
+
result.paidAt = data.paidAt;
|
|
565
|
+
const method = this.#mapPaymentMethodType(data.method);
|
|
566
|
+
if (method !== undefined)
|
|
567
|
+
result.method = method;
|
|
568
|
+
const checkout = data._links?.checkout?.href;
|
|
569
|
+
if (checkout !== undefined)
|
|
570
|
+
result.hostedUrl = checkout;
|
|
571
|
+
return result;
|
|
572
|
+
}
|
|
573
|
+
#mapSubscription(data) {
|
|
574
|
+
this.#subscriptionCustomers.set(data.id, data.customerId);
|
|
575
|
+
const statusMap = {
|
|
576
|
+
pending: 'incomplete',
|
|
577
|
+
active: 'active',
|
|
578
|
+
canceled: 'canceled',
|
|
579
|
+
suspended: 'past_due',
|
|
580
|
+
completed: 'ended',
|
|
581
|
+
};
|
|
582
|
+
const planId = data.metadata?.planId;
|
|
583
|
+
return {
|
|
584
|
+
id: data.id,
|
|
585
|
+
gatewayId: data.id,
|
|
586
|
+
provider: this.provider,
|
|
587
|
+
customerId: data.customerId,
|
|
588
|
+
status: statusMap[data.status] ?? 'active',
|
|
589
|
+
planId: typeof planId === 'string' ? planId : (data.description ?? data.interval),
|
|
590
|
+
amount: this.#fromMollieAmount(data.amount),
|
|
591
|
+
...(data.canceledAt != null ? { endsAt: data.canceledAt } : {}),
|
|
592
|
+
...(data.nextPaymentDate != null ? { currentPeriodEnd: data.nextPaymentDate } : {}),
|
|
593
|
+
payload: data,
|
|
594
|
+
createdAt: data.createdAt,
|
|
595
|
+
};
|
|
596
|
+
}
|
|
597
|
+
/**
|
|
598
|
+
* Canonical method name → what goes in Mollie's `method` field. Anything else is a
|
|
599
|
+
* routing mistake and fails here rather than at the gateway.
|
|
600
|
+
*/
|
|
601
|
+
#mapMethod(method, metadata) {
|
|
602
|
+
const brand = metadata?.mollieMethod;
|
|
603
|
+
const category = method === undefined || method === 'undefined' ? undefined : method;
|
|
604
|
+
if (typeof brand === 'string') {
|
|
605
|
+
if (category !== undefined) {
|
|
606
|
+
const allowed = MOLLIE_METHOD_CATEGORIES[category];
|
|
607
|
+
if (allowed === undefined)
|
|
608
|
+
throw this.#unknownMethod(category);
|
|
609
|
+
if (!allowed.includes(brand)) {
|
|
610
|
+
throw new Error(`[payments] Mollie method "${brand}" is not a \`${category}\` method. \`metadata.mollieMethod\` names the exact Mollie id and must belong to the category being routed — ${category} covers: ${allowed.join(', ')}.`);
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
return brand;
|
|
614
|
+
}
|
|
615
|
+
if (category === undefined)
|
|
616
|
+
return undefined;
|
|
617
|
+
const ids = MOLLIE_METHOD_CATEGORIES[category];
|
|
618
|
+
if (ids === undefined)
|
|
619
|
+
throw this.#unknownMethod(category);
|
|
620
|
+
// A single-member category goes out as the bare id Mollie documents, not a 1-element
|
|
621
|
+
// array, so the request body reads the way Mollie's own examples do.
|
|
622
|
+
return ids.length === 1 ? ids[0] : [...ids];
|
|
623
|
+
}
|
|
624
|
+
#unknownMethod(method) {
|
|
625
|
+
return new Error(`[payments] Mollie has no "${method}" method. This driver routes the categories ${Object.keys(MOLLIE_METHOD_CATEGORIES).join(', ')}, or lets the shopper pick on the hosted page when no method is given. Mollie has no separate debit-card method (debit cards are \`creditcard\`), and no Pix or boleto.`);
|
|
626
|
+
}
|
|
627
|
+
/**
|
|
628
|
+
* A Mollie method id, as reported back on a payment, onto the contract's categories.
|
|
629
|
+
* Only `creditcard` had a name before, so an iDEAL or Klarna payment came back with no
|
|
630
|
+
* `method` at all.
|
|
631
|
+
*/
|
|
632
|
+
#mapPaymentMethodType(method) {
|
|
633
|
+
if (method === null || method === undefined)
|
|
634
|
+
return undefined;
|
|
635
|
+
if (method === 'creditcard' || method === 'pointofsale')
|
|
636
|
+
return 'card';
|
|
637
|
+
for (const [category, ids] of Object.entries(MOLLIE_METHOD_CATEGORIES)) {
|
|
638
|
+
if (!ids.includes(method))
|
|
639
|
+
continue;
|
|
640
|
+
if (category === 'credit_card')
|
|
641
|
+
return 'card';
|
|
642
|
+
return category;
|
|
643
|
+
}
|
|
644
|
+
// A method Mollie added since this table was written: it was paid, and saying so with
|
|
645
|
+
// `unknown` is honest, where leaving it unset would read as "Mollie did not tell us".
|
|
646
|
+
return 'unknown';
|
|
647
|
+
}
|
|
648
|
+
#mapCycle(cycle) {
|
|
649
|
+
switch (cycle) {
|
|
650
|
+
case 'WEEKLY':
|
|
651
|
+
return '1 week';
|
|
652
|
+
case 'BIWEEKLY':
|
|
653
|
+
return '2 weeks';
|
|
654
|
+
case 'QUARTERLY':
|
|
655
|
+
return '3 months';
|
|
656
|
+
case 'SEMIANNUALLY':
|
|
657
|
+
return '6 months';
|
|
658
|
+
case 'YEARLY':
|
|
659
|
+
return '12 months';
|
|
660
|
+
default:
|
|
661
|
+
return '1 month';
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
#mapEventType(type) {
|
|
665
|
+
switch (type) {
|
|
666
|
+
case 'payment.paid':
|
|
667
|
+
return 'payment.succeeded';
|
|
668
|
+
case 'payment.failed':
|
|
669
|
+
case 'payment.expired':
|
|
670
|
+
case 'payment.canceled':
|
|
671
|
+
return 'payment.failed';
|
|
672
|
+
case 'payment.refunded':
|
|
673
|
+
case 'refund.successful':
|
|
674
|
+
return 'payment.refunded';
|
|
675
|
+
case 'chargeback.received':
|
|
676
|
+
return 'payment.disputed';
|
|
677
|
+
default:
|
|
678
|
+
return type ?? 'payment.updated';
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
/**
|
|
682
|
+
* The event-id suffix for a payment webhook. Mollie's own `status` is the transition in
|
|
683
|
+
* every ordinary case, but it stays `paid` through both a refund and a chargeback — so
|
|
684
|
+
* those two get their own suffix, or their event id collides with the earlier
|
|
685
|
+
* `payment.succeeded` one and the idempotency ledger drops them as replays.
|
|
686
|
+
*/
|
|
687
|
+
#transition(gatewayStatus, mapped) {
|
|
688
|
+
if (mapped === 'disputed')
|
|
689
|
+
return `${gatewayStatus}:chargeback`;
|
|
690
|
+
if (mapped === 'refunded')
|
|
691
|
+
return `${gatewayStatus}:refunded`;
|
|
692
|
+
return gatewayStatus;
|
|
693
|
+
}
|
|
694
|
+
// ── Helpers ──────────────────────────────────────────────────────────────────────────
|
|
695
|
+
/**
|
|
696
|
+
* Mollie takes money as a decimal string carrying the currency's own number of decimals
|
|
697
|
+
* — `"19.90"` for EUR, `"1990"` for JPY, `"1.990"` for KWD. `formatDecimal` shifts the
|
|
698
|
+
* digits of the integer instead of dividing, so nothing is lost to a binary float and a
|
|
699
|
+
* zero-decimal currency is not billed at a hundredth of its amount.
|
|
700
|
+
*/
|
|
701
|
+
#toMollieAmount(amount, currency) {
|
|
702
|
+
const resolved = currency ?? this.#currency;
|
|
703
|
+
return { currency: resolved.toUpperCase(), value: formatDecimal(amount, resolved) };
|
|
704
|
+
}
|
|
705
|
+
#fromMollieAmount(amount) {
|
|
706
|
+
return {
|
|
707
|
+
amount: fromDecimal(Number(amount.value), amount.currency),
|
|
708
|
+
currency: amount.currency.toLowerCase(),
|
|
709
|
+
};
|
|
710
|
+
}
|
|
711
|
+
/**
|
|
712
|
+
* Pull the per-request URLs out of `metadata`: `ChargeInput` has no room for the redirect
|
|
713
|
+
* and webhook URLs Mollie needs, and echoing them back as gateway metadata would be noise.
|
|
714
|
+
*/
|
|
715
|
+
#transportOptions(metadata) {
|
|
716
|
+
// `mollieMethod` is read as an argument (the exact Mollie method id to pin), so it is
|
|
717
|
+
// consumed here rather than echoed back as gateway metadata.
|
|
718
|
+
const { redirectUrl, cancelUrl, webhookUrl, mollieMethod: _mollieMethod, ...rest } = metadata ?? {};
|
|
719
|
+
return {
|
|
720
|
+
...(typeof redirectUrl === 'string' ? { redirectUrl } : {}),
|
|
721
|
+
...(typeof cancelUrl === 'string' ? { cancelUrl } : {}),
|
|
722
|
+
...(typeof webhookUrl === 'string' ? { webhookUrl } : {}),
|
|
723
|
+
rest,
|
|
724
|
+
};
|
|
725
|
+
}
|
|
726
|
+
#metadataBody(rest, externalReference) {
|
|
727
|
+
const metadata = {
|
|
728
|
+
...rest,
|
|
729
|
+
...(externalReference !== undefined ? { externalReference } : {}),
|
|
730
|
+
};
|
|
731
|
+
return Object.keys(metadata).length > 0 ? { metadata } : {};
|
|
732
|
+
}
|
|
733
|
+
#customerMetadata(input) {
|
|
734
|
+
// Mollie's customer has no tax-id field, so it rides along in metadata — the one place
|
|
735
|
+
// it survives a round-trip, and where `#mapCustomer` reads it back from.
|
|
736
|
+
const metadata = {
|
|
737
|
+
...(input.metadata ?? {}),
|
|
738
|
+
...(input.taxId !== undefined ? { taxId: input.taxId } : {}),
|
|
739
|
+
};
|
|
740
|
+
return Object.keys(metadata).length > 0 ? { metadata } : {};
|
|
741
|
+
}
|
|
742
|
+
/**
|
|
743
|
+
* Mollie deduplicates on the `Idempotency-Key` request header and on nothing else — a key
|
|
744
|
+
* written into `metadata` would be echoed back and protect nothing. Mollie keeps a key for
|
|
745
|
+
* one hour; after that the same key creates a second payment.
|
|
746
|
+
*
|
|
747
|
+
* Applied to every POST the driver makes: `charge`, `createCheckout`, `refund`,
|
|
748
|
+
* `createCustomer` and `createSubscription`. `updateSubscription` is a PATCH, which
|
|
749
|
+
* Mollie does not accept keys for at all, so it refuses instead.
|
|
750
|
+
*/
|
|
751
|
+
#idempotency(key) {
|
|
752
|
+
return key !== undefined ? { headers: { 'Idempotency-Key': key } } : {};
|
|
753
|
+
}
|
|
754
|
+
/**
|
|
755
|
+
* Every Mollie subscription endpoint is nested under a customer, but the driver contract
|
|
756
|
+
* passes only the subscription id. Resolved from the pairing this process already saw,
|
|
757
|
+
* or from an explicit `customerId/subscriptionId` — never guessed.
|
|
758
|
+
*/
|
|
759
|
+
async #resolveSubscription(gatewayId) {
|
|
760
|
+
const slash = gatewayId.indexOf('/');
|
|
761
|
+
if (slash !== -1) {
|
|
762
|
+
return {
|
|
763
|
+
customerId: gatewayId.slice(0, slash),
|
|
764
|
+
subscriptionId: gatewayId.slice(slash + 1),
|
|
765
|
+
};
|
|
766
|
+
}
|
|
767
|
+
const known = this.#subscriptionCustomers.get(gatewayId);
|
|
768
|
+
if (known !== undefined)
|
|
769
|
+
return { customerId: known, subscriptionId: gatewayId };
|
|
770
|
+
// Last resort: Mollie's account-wide subscription list, which does carry `customerId`.
|
|
771
|
+
const page = await this.#request('/subscriptions?limit=250');
|
|
772
|
+
const found = page._embedded?.subscriptions?.find((item) => item.id === gatewayId);
|
|
773
|
+
if (found !== undefined) {
|
|
774
|
+
this.#subscriptionCustomers.set(found.id, found.customerId);
|
|
775
|
+
return { customerId: found.customerId, subscriptionId: found.id };
|
|
776
|
+
}
|
|
777
|
+
throw new Error(`[payments] Mollie subscription ${gatewayId} needs its customer: every subscription endpoint is nested under one, and it was not in the first page of the account-wide list. Pass it as \`"cst_xxx/sub_xxx"\`.`);
|
|
778
|
+
}
|
|
779
|
+
/** Next-gen webhook envelope, or `null` when this is the classic form-encoded id POST. */
|
|
780
|
+
#parseEventPayload(rawBody) {
|
|
781
|
+
if (!rawBody.trimStart().startsWith('{'))
|
|
782
|
+
return null;
|
|
783
|
+
const parsed = JSON.parse(rawBody);
|
|
784
|
+
return parsed.resource === 'event' ? parsed : null;
|
|
785
|
+
}
|
|
786
|
+
#verifySignature(rawBody, signature) {
|
|
787
|
+
if (signature === undefined || signature === '') {
|
|
788
|
+
throw new Error('[payments] Missing X-Mollie-Signature on Mollie webhook request.');
|
|
789
|
+
}
|
|
790
|
+
// Mollie sends `sha256=<hex>`; the digest is over the raw, undeserialized body.
|
|
791
|
+
const received = signature.startsWith('sha256=') ? signature.slice(7) : signature;
|
|
792
|
+
const secret = this.#webhookSecret ?? '';
|
|
793
|
+
const expected = createHmac('sha256', secret).update(rawBody, 'utf8').digest('hex');
|
|
794
|
+
if (!safeCompare(received, expected)) {
|
|
795
|
+
throw new Error('[payments] Invalid Mollie webhook signature.');
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
/** The classic webhook's only field: `id=tr_xxx`, form-encoded. */
|
|
799
|
+
#classicId(rawBody) {
|
|
800
|
+
return new URLSearchParams(rawBody).get('id') ?? undefined;
|
|
801
|
+
}
|
|
802
|
+
async #request(path, options = {}) {
|
|
803
|
+
return httpRequest(path, {
|
|
804
|
+
baseUrl: this.#baseUrl,
|
|
805
|
+
...(options.method !== undefined ? { method: options.method } : {}),
|
|
806
|
+
...(options.body !== undefined ? { body: options.body } : {}),
|
|
807
|
+
...(options.headers !== undefined ? { headers: options.headers } : {}),
|
|
808
|
+
bearerToken: this.#apiKey,
|
|
809
|
+
});
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
//# sourceMappingURL=mollie.js.map
|