@adonis-agora/payments 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +2347 -0
- package/dist/assets/spa/assets/index-CT5Zon6P.css +1 -0
- package/dist/assets/spa/assets/index-CvEbJEnq.js +41 -0
- package/dist/assets/spa/assets/index-CvEbJEnq.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/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/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 +2 -1
- package/dist/src/billing/index.d.ts.map +1 -1
- package/dist/src/billing/index.js +1 -0
- 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 +3 -0
- package/dist/src/billing/mixins/index.d.ts.map +1 -1
- package/dist/src/billing/mixins/index.js +3 -0
- 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_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 +16 -0
- package/dist/src/client/index.d.ts.map +1 -0
- package/dist/src/client/index.js +13 -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 +18 -0
- package/dist/src/dashboard/index.d.ts.map +1 -0
- package/dist/src/dashboard/index.js +14 -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 +244 -9
- 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 +12 -2
- 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 +52 -2
- 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 +40 -3
- package/dist/src/drivers/stripe.d.ts.map +1 -1
- package/dist/src/drivers/stripe.js +806 -25
- package/dist/src/drivers/stripe.js.map +1 -1
- package/dist/src/drivers/woovi.d.ts +10 -3
- 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/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 +40 -10
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +22 -4
- package/dist/src/index.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/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 +65 -13
- 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/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 +2 -1
- 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/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/make/webhook_handler/main.stub +29 -11
- package/package.json +60 -25
|
@@ -0,0 +1,844 @@
|
|
|
1
|
+
import { createHmac, randomUUID } 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 { fromDecimal, toDecimal } from '../money.js';
|
|
6
|
+
import { safeCompare } from '../webhook_security.js';
|
|
7
|
+
import { requireCredential, requireCurrency } from './shared.js';
|
|
8
|
+
/** Mercado Pago rejects an `external_reference` outside this shape (max 64 chars). */
|
|
9
|
+
const EXTERNAL_REFERENCE_PATTERN = /^[A-Za-z0-9_-]{1,64}$/;
|
|
10
|
+
/**
|
|
11
|
+
* Mercado Pago driver — the largest gateway in Latin America, over the Checkout API
|
|
12
|
+
* (`/v1/payments`), Checkout Pro preferences and preapproval subscriptions. Plain REST,
|
|
13
|
+
* no SDK.
|
|
14
|
+
*
|
|
15
|
+
* Unlike the BRL-only Brazilian drivers this one is multi-currency across BR/AR/MX/CL/
|
|
16
|
+
* CO/PE/UY, so `currency` is required. Money crosses the boundary as a decimal
|
|
17
|
+
* `transaction_amount`.
|
|
18
|
+
*
|
|
19
|
+
* The one thing to know before reading {@link parseWebhook}: a Mercado Pago notification
|
|
20
|
+
* carries **only the id of the resource that changed**. Nothing else — no amount, no
|
|
21
|
+
* status, no `external_reference`.
|
|
22
|
+
*/
|
|
23
|
+
export class MercadoPagoDriver {
|
|
24
|
+
provider = 'mercadopago';
|
|
25
|
+
/**
|
|
26
|
+
* What `charge()` actually creates. `undefined` is absent on purpose: Mercado Pago
|
|
27
|
+
* requires a `payment_method_id` on every payment, so "let the customer choose" is the
|
|
28
|
+
* Checkout Pro flow ({@link createCheckout}), not a charge.
|
|
29
|
+
*/
|
|
30
|
+
supportedMethods = ['pix', 'boleto', 'credit_card', 'debit_card'];
|
|
31
|
+
capabilities = { refunds: true, invoices: false, subscriptions: true };
|
|
32
|
+
#baseUrl = 'https://api.mercadopago.com';
|
|
33
|
+
#accessToken;
|
|
34
|
+
#currency;
|
|
35
|
+
#webhookSecret;
|
|
36
|
+
#invoiceCtx;
|
|
37
|
+
constructor(ctx, config) {
|
|
38
|
+
this.#invoiceCtx = ctx;
|
|
39
|
+
this.#accessToken = requireCredential({
|
|
40
|
+
driver: 'mercadopago',
|
|
41
|
+
option: 'accessToken',
|
|
42
|
+
env: 'MERCADOPAGO_ACCESS_TOKEN',
|
|
43
|
+
value: config.accessToken,
|
|
44
|
+
});
|
|
45
|
+
this.#currency = requireCurrency('mercadopago', config.currency);
|
|
46
|
+
this.#webhookSecret = config.webhookSecret ?? process.env.MERCADOPAGO_WEBHOOK_SECRET;
|
|
47
|
+
}
|
|
48
|
+
// ── Customers ────────────────────────────────────────────────────────────────────────
|
|
49
|
+
async createCustomer(input) {
|
|
50
|
+
this.#refuseIdempotencyKey(input.idempotencyKey, 'createCustomer', 'POST /v1/customers');
|
|
51
|
+
const [firstName, ...rest] = (input.name ?? '').split(' ');
|
|
52
|
+
const body = {
|
|
53
|
+
...(input.email !== undefined ? { email: input.email } : {}),
|
|
54
|
+
...(firstName !== undefined && firstName !== '' ? { first_name: firstName } : {}),
|
|
55
|
+
...(rest.length > 0 ? { last_name: rest.join(' ') } : {}),
|
|
56
|
+
...(input.taxId !== undefined ? { identification: this.#identification(input.taxId) } : {}),
|
|
57
|
+
...(input.metadata !== undefined ? input.metadata : {}),
|
|
58
|
+
};
|
|
59
|
+
const data = await this.#request('/v1/customers', {
|
|
60
|
+
method: 'POST',
|
|
61
|
+
body,
|
|
62
|
+
});
|
|
63
|
+
return this.#mapCustomer(data);
|
|
64
|
+
}
|
|
65
|
+
async findCustomer(customerId) {
|
|
66
|
+
try {
|
|
67
|
+
const data = await this.#request(`/v1/customers/${customerId}`);
|
|
68
|
+
return this.#mapCustomer(data);
|
|
69
|
+
}
|
|
70
|
+
catch (error) {
|
|
71
|
+
if (isNotFound(error))
|
|
72
|
+
return null;
|
|
73
|
+
throw error;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
async updateCustomer(customerId, input) {
|
|
77
|
+
const [firstName, ...rest] = (input.name ?? '').split(' ');
|
|
78
|
+
const body = {
|
|
79
|
+
// Mercado Pago only accepts `email` on a customer that has none yet; sending it for
|
|
80
|
+
// one that already has an email comes back as error 126.
|
|
81
|
+
...(input.email !== undefined ? { email: input.email } : {}),
|
|
82
|
+
...(firstName !== undefined && firstName !== '' ? { first_name: firstName } : {}),
|
|
83
|
+
...(rest.length > 0 ? { last_name: rest.join(' ') } : {}),
|
|
84
|
+
...(input.taxId !== undefined ? { identification: this.#identification(input.taxId) } : {}),
|
|
85
|
+
...(input.metadata !== undefined ? input.metadata : {}),
|
|
86
|
+
};
|
|
87
|
+
const data = await this.#request(`/v1/customers/${customerId}`, {
|
|
88
|
+
method: 'PUT',
|
|
89
|
+
body,
|
|
90
|
+
});
|
|
91
|
+
return this.#mapCustomer(data);
|
|
92
|
+
}
|
|
93
|
+
// ── Payments ─────────────────────────────────────────────────────────────────────────
|
|
94
|
+
async charge(input) {
|
|
95
|
+
const currency = input.currency ?? this.#currency;
|
|
96
|
+
const token = input.card?.token ?? input.paymentMethodId;
|
|
97
|
+
const paymentMethodId = this.#paymentMethodId(input, token !== undefined);
|
|
98
|
+
if (input.externalReference !== undefined) {
|
|
99
|
+
this.#assertExternalReference(input.externalReference);
|
|
100
|
+
}
|
|
101
|
+
const body = {
|
|
102
|
+
transaction_amount: this.#toAmount(input.amount, currency),
|
|
103
|
+
payment_method_id: paymentMethodId,
|
|
104
|
+
payer: this.#payer(input),
|
|
105
|
+
...(input.description !== undefined ? { description: input.description } : {}),
|
|
106
|
+
...(input.externalReference !== undefined
|
|
107
|
+
? { external_reference: input.externalReference }
|
|
108
|
+
: {}),
|
|
109
|
+
...(token !== undefined
|
|
110
|
+
? {
|
|
111
|
+
token,
|
|
112
|
+
// Mercado Pago rejects a card payment with no `installments` (error 4004);
|
|
113
|
+
// one is the single-payment case, not a guess at the shopper's choice.
|
|
114
|
+
installments: this.#numberOption(input.metadata?.installments) ?? 1,
|
|
115
|
+
...(typeof input.metadata?.issuerId === 'string'
|
|
116
|
+
? { issuer_id: input.metadata.issuerId }
|
|
117
|
+
: {}),
|
|
118
|
+
}
|
|
119
|
+
: {}),
|
|
120
|
+
...(typeof input.metadata?.dateOfExpiration === 'string'
|
|
121
|
+
? { date_of_expiration: input.metadata.dateOfExpiration }
|
|
122
|
+
: {}),
|
|
123
|
+
...(typeof input.metadata?.notificationUrl === 'string'
|
|
124
|
+
? { notification_url: input.metadata.notificationUrl }
|
|
125
|
+
: {}),
|
|
126
|
+
...(input.metadata !== undefined ? { metadata: input.metadata } : {}),
|
|
127
|
+
};
|
|
128
|
+
const data = await this.#request('/v1/payments', {
|
|
129
|
+
method: 'POST',
|
|
130
|
+
body,
|
|
131
|
+
// Mercado Pago requires the header on every payment. A key the caller did not give
|
|
132
|
+
// us cannot deduplicate their retry — pass `idempotencyKey` to actually get that.
|
|
133
|
+
idempotencyKey: input.idempotencyKey ?? randomUUID(),
|
|
134
|
+
});
|
|
135
|
+
const payment = this.#mapPayment(data, currency);
|
|
136
|
+
await emitInvoiceIfRequested(this.#invoiceCtx, input, payment, this);
|
|
137
|
+
publishPaymentDiagnostics(payment);
|
|
138
|
+
return payment;
|
|
139
|
+
}
|
|
140
|
+
async findPayment(gatewayId) {
|
|
141
|
+
try {
|
|
142
|
+
const data = await this.#request(`/v1/payments/${gatewayId}`);
|
|
143
|
+
return this.#mapPayment(data, this.#currency);
|
|
144
|
+
}
|
|
145
|
+
catch (error) {
|
|
146
|
+
if (isNotFound(error))
|
|
147
|
+
return null;
|
|
148
|
+
throw error;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
async refund(paymentGatewayId, amount, options) {
|
|
152
|
+
const currency = this.#currency;
|
|
153
|
+
// No `amount` is a full refund, per Mercado Pago's own wording.
|
|
154
|
+
const body = amount !== undefined ? { amount: this.#toAmount(amount, currency) } : {};
|
|
155
|
+
const data = await this.#request(`/v1/payments/${paymentGatewayId}/refunds`, {
|
|
156
|
+
method: 'POST',
|
|
157
|
+
body,
|
|
158
|
+
// The header is required here too, so a caller's key finally reaches it. The
|
|
159
|
+
// generated fallback stays for the callers who pass none, and it is deliberately
|
|
160
|
+
// random rather than derived from the payment id: two partial refunds of the same
|
|
161
|
+
// amount are a normal thing to want, and a per-payment key would collapse the
|
|
162
|
+
// second into the first and report a success for a refund never made.
|
|
163
|
+
idempotencyKey: options?.idempotencyKey ?? randomUUID(),
|
|
164
|
+
});
|
|
165
|
+
const refund = {
|
|
166
|
+
id: String(data.id),
|
|
167
|
+
gatewayId: String(data.id),
|
|
168
|
+
provider: this.provider,
|
|
169
|
+
amount: { amount: this.#fromAmount(data.amount, currency), currency },
|
|
170
|
+
status: data.status === 'approved'
|
|
171
|
+
? 'succeeded'
|
|
172
|
+
: data.status === 'rejected' || data.status === 'cancelled'
|
|
173
|
+
? 'failed'
|
|
174
|
+
: 'pending',
|
|
175
|
+
createdAt: data.date_created ?? new Date().toISOString(),
|
|
176
|
+
};
|
|
177
|
+
publishRefundDiagnostics(refund);
|
|
178
|
+
return refund;
|
|
179
|
+
}
|
|
180
|
+
// ── Checkout ─────────────────────────────────────────────────────────────────────────
|
|
181
|
+
/**
|
|
182
|
+
* Checkout Pro: a hosted preference the payer completes on Mercado Pago, choosing the
|
|
183
|
+
* method there. `back_urls` must be https — Mercado Pago silently discards http ones.
|
|
184
|
+
*/
|
|
185
|
+
async createCheckout(input) {
|
|
186
|
+
const currency = input.currency ?? this.#currency;
|
|
187
|
+
const externalReference = input.externalReference;
|
|
188
|
+
if (externalReference !== undefined)
|
|
189
|
+
this.#assertExternalReference(externalReference);
|
|
190
|
+
const body = {
|
|
191
|
+
items: [
|
|
192
|
+
{
|
|
193
|
+
title: input.description ?? 'Payment',
|
|
194
|
+
quantity: 1,
|
|
195
|
+
currency_id: currency.toUpperCase(),
|
|
196
|
+
unit_price: this.#toAmount(input.amount, currency),
|
|
197
|
+
},
|
|
198
|
+
],
|
|
199
|
+
back_urls: {
|
|
200
|
+
success: input.successUrl,
|
|
201
|
+
pending: input.successUrl,
|
|
202
|
+
...(input.cancelUrl !== undefined ? { failure: input.cancelUrl } : {}),
|
|
203
|
+
},
|
|
204
|
+
auto_return: 'approved',
|
|
205
|
+
...(externalReference !== undefined ? { external_reference: externalReference } : {}),
|
|
206
|
+
...(input.metadata !== undefined ? { metadata: input.metadata } : {}),
|
|
207
|
+
};
|
|
208
|
+
const data = await this.#request('/checkout/preferences', {
|
|
209
|
+
method: 'POST',
|
|
210
|
+
body,
|
|
211
|
+
});
|
|
212
|
+
return {
|
|
213
|
+
id: data.id,
|
|
214
|
+
gatewayId: data.id,
|
|
215
|
+
provider: this.provider,
|
|
216
|
+
// `sandbox_init_point` is documented as "do not use" — test credentials go through
|
|
217
|
+
// `init_point` too.
|
|
218
|
+
url: data.init_point ?? '',
|
|
219
|
+
status: 'open',
|
|
220
|
+
amount: { amount: input.amount, currency },
|
|
221
|
+
...(input.customerId !== undefined ? { customerId: input.customerId } : {}),
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
// ── Subscriptions ────────────────────────────────────────────────────────────────────
|
|
225
|
+
/**
|
|
226
|
+
* A preapproval. With an `amount` the driver creates a subscription with an inline
|
|
227
|
+
* recurrence; without one, `planId` is used as an existing `preapproval_plan_id` and the
|
|
228
|
+
* plan defines the price and the cycle.
|
|
229
|
+
*/
|
|
230
|
+
async createSubscription(input) {
|
|
231
|
+
this.#refuseIdempotencyKey(input.idempotencyKey, 'createSubscription', 'POST /preapproval');
|
|
232
|
+
const currency = this.#currency;
|
|
233
|
+
if (input.externalReference !== undefined) {
|
|
234
|
+
this.#assertExternalReference(input.externalReference);
|
|
235
|
+
}
|
|
236
|
+
const endDate = typeof input.metadata?.endDate === 'string' ? input.metadata.endDate : undefined;
|
|
237
|
+
// Mercado Pago documents that `start_date` is only honoured alongside `end_date`.
|
|
238
|
+
// Sending it alone would start the billing on a date the gateway silently ignores.
|
|
239
|
+
if (input.startDate !== undefined && endDate === undefined) {
|
|
240
|
+
throw new Error('[payments] Mercado Pago ignores a subscription `start_date` unless an end date is ' +
|
|
241
|
+
'sent with it. Pass `metadata.endDate` (ISO) alongside `startDate`, or drop ' +
|
|
242
|
+
'`startDate` and let it start now.');
|
|
243
|
+
}
|
|
244
|
+
const email = input.customer?.email ?? input.card?.holder?.email;
|
|
245
|
+
const body = {
|
|
246
|
+
...(input.amount !== undefined
|
|
247
|
+
? {
|
|
248
|
+
reason: input.description ?? 'Subscription',
|
|
249
|
+
auto_recurring: {
|
|
250
|
+
...this.#recurrence(input.cycle),
|
|
251
|
+
transaction_amount: this.#toAmount(input.amount, currency),
|
|
252
|
+
currency_id: currency.toUpperCase(),
|
|
253
|
+
...(input.startDate !== undefined ? { start_date: input.startDate } : {}),
|
|
254
|
+
...(endDate !== undefined ? { end_date: endDate } : {}),
|
|
255
|
+
...(input.trialDays !== undefined
|
|
256
|
+
? { free_trial: { frequency: input.trialDays, frequency_type: 'days' } }
|
|
257
|
+
: {}),
|
|
258
|
+
},
|
|
259
|
+
}
|
|
260
|
+
: { preapproval_plan_id: input.planId }),
|
|
261
|
+
...(email !== undefined ? { payer_email: email } : {}),
|
|
262
|
+
...(input.card !== undefined ? { card_token_id: input.card.token } : {}),
|
|
263
|
+
...(input.externalReference !== undefined
|
|
264
|
+
? { external_reference: input.externalReference }
|
|
265
|
+
: {}),
|
|
266
|
+
...(typeof input.metadata?.backUrl === 'string' ? { back_url: input.metadata.backUrl } : {}),
|
|
267
|
+
// `authorized` needs a payment method; without a card the payer authorizes it at the
|
|
268
|
+
// `init_point` and Mercado Pago flips it itself.
|
|
269
|
+
status: input.card !== undefined ? 'authorized' : 'pending',
|
|
270
|
+
};
|
|
271
|
+
if (input.amount !== undefined && body.back_url === undefined) {
|
|
272
|
+
throw new Error('[payments] Mercado Pago requires `back_url` on a subscription created without a ' +
|
|
273
|
+
'plan — pass `metadata.backUrl`, or create a preapproval plan and pass its id as ' +
|
|
274
|
+
'`planId` with no `amount`.');
|
|
275
|
+
}
|
|
276
|
+
const data = await this.#request('/preapproval', {
|
|
277
|
+
method: 'POST',
|
|
278
|
+
body,
|
|
279
|
+
});
|
|
280
|
+
const subscription = this.#mapSubscription(data);
|
|
281
|
+
publishSubscriptionDiagnostics(subscription, 'subscription.created');
|
|
282
|
+
return subscription;
|
|
283
|
+
}
|
|
284
|
+
async cancelSubscription(subscriptionGatewayId, options) {
|
|
285
|
+
if (options?.atPeriodEnd === true) {
|
|
286
|
+
throw new Error('[payments] Mercado Pago cancels a preapproval immediately and irreversibly — there ' +
|
|
287
|
+
'is no cancel-at-period-end flag. Pause it instead (`status: "paused"`) and cancel ' +
|
|
288
|
+
'when the period ends, or keep the grace period on your own records.');
|
|
289
|
+
}
|
|
290
|
+
const data = await this.#request(`/preapproval/${subscriptionGatewayId}`,
|
|
291
|
+
// Preapproval spells it `canceled` with one L, unlike a payment's `cancelled`.
|
|
292
|
+
{ method: 'PUT', body: { status: 'canceled' } });
|
|
293
|
+
const subscription = this.#mapSubscription(data);
|
|
294
|
+
publishSubscriptionDiagnostics(subscription, 'subscription.canceled');
|
|
295
|
+
return subscription;
|
|
296
|
+
}
|
|
297
|
+
async updateSubscription(subscriptionGatewayId, input) {
|
|
298
|
+
this.#refuseIdempotencyKey(input.idempotencyKey, 'updateSubscription', 'PUT /preapproval/{id}');
|
|
299
|
+
const currency = this.#currency;
|
|
300
|
+
const body = {
|
|
301
|
+
...(input.amount !== undefined
|
|
302
|
+
? {
|
|
303
|
+
auto_recurring: {
|
|
304
|
+
transaction_amount: this.#toAmount(input.amount, currency),
|
|
305
|
+
currency_id: currency.toUpperCase(),
|
|
306
|
+
},
|
|
307
|
+
}
|
|
308
|
+
: {}),
|
|
309
|
+
// `reason` is the subscription's own description, the one the payer sees.
|
|
310
|
+
...(input.description !== undefined ? { reason: input.description } : {}),
|
|
311
|
+
};
|
|
312
|
+
const data = await this.#request(`/preapproval/${subscriptionGatewayId}`, { method: 'PUT', body });
|
|
313
|
+
return this.#mapSubscription(data);
|
|
314
|
+
}
|
|
315
|
+
async findSubscription(gatewayId) {
|
|
316
|
+
try {
|
|
317
|
+
const data = await this.#request(`/preapproval/${gatewayId}`);
|
|
318
|
+
return this.#mapSubscription(data);
|
|
319
|
+
}
|
|
320
|
+
catch (error) {
|
|
321
|
+
if (isNotFound(error))
|
|
322
|
+
return null;
|
|
323
|
+
throw error;
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
// ── Invoices ─────────────────────────────────────────────────────────────────────────
|
|
327
|
+
async listInvoices(_customerId) {
|
|
328
|
+
throw new Error('[payments] Mercado Pago has no invoices for a customer. `GET /authorized_payments/' +
|
|
329
|
+
'search` lists the charges of a *subscription* and filters by `payer_id`, which is a ' +
|
|
330
|
+
'Mercado Pago user id, not the `/v1/customers` id. Use `findPayment`, or an invoice ' +
|
|
331
|
+
'provider with `invoice: true`.');
|
|
332
|
+
}
|
|
333
|
+
// ── Webhooks ─────────────────────────────────────────────────────────────────────────
|
|
334
|
+
/**
|
|
335
|
+
* Verify the `x-signature` HMAC, then fetch the resource the notification names.
|
|
336
|
+
*
|
|
337
|
+
* A Mercado Pago notification is an id and nothing more — no amount, no status, no
|
|
338
|
+
* `external_reference` — so the fetch is not an optimization to skip: without it this
|
|
339
|
+
* could only ever return `payment.updated`, and a payment that settled would be
|
|
340
|
+
* ledgered and never marked paid. It costs one API call per notification, which is
|
|
341
|
+
* Mercado Pago's design rather than an inefficiency of this driver.
|
|
342
|
+
*
|
|
343
|
+
* A failed fetch throws, so the mounted route answers 400 and Mercado Pago retries.
|
|
344
|
+
* Reporting a status the gateway did not confirm would be worse than a retry.
|
|
345
|
+
*/
|
|
346
|
+
/**
|
|
347
|
+
* Whether a delivery to `POST /payments/webhook/:provider` can be authenticated.
|
|
348
|
+
*
|
|
349
|
+
* Without the secret `#verify` hands the payload back unchecked.
|
|
350
|
+
*/
|
|
351
|
+
get webhookVerification() {
|
|
352
|
+
return this.#webhookSecret !== undefined ? 'configured' : 'unconfigured';
|
|
353
|
+
}
|
|
354
|
+
async parseWebhook(rawBody, headers) {
|
|
355
|
+
const payload = this.#verified(rawBody, headers);
|
|
356
|
+
const type = payload.type ?? '';
|
|
357
|
+
const resourceId = payload.data?.id === undefined ? '' : String(payload.data.id);
|
|
358
|
+
const event = {
|
|
359
|
+
id: payload.id !== undefined ? String(payload.id) : `${payload.action ?? type}-${resourceId}`,
|
|
360
|
+
provider: this.provider,
|
|
361
|
+
type: this.#mapWebhookType(type),
|
|
362
|
+
createdAt: payload.date_created ?? new Date().toISOString(),
|
|
363
|
+
data: { gatewayId: resourceId },
|
|
364
|
+
raw: payload,
|
|
365
|
+
};
|
|
366
|
+
if (resourceId === '')
|
|
367
|
+
return event;
|
|
368
|
+
if (type === 'payment') {
|
|
369
|
+
return this.#paymentEvent(event, resourceId);
|
|
370
|
+
}
|
|
371
|
+
// A chargeback case: opened, or its status changed. Mercado Pago sends one action for
|
|
372
|
+
// both (`actions: ["changed_case_status"]`), so the notification itself cannot say
|
|
373
|
+
// whether this is the opening — the payment can, and it is the payment this library
|
|
374
|
+
// has a row for. `data.id` here is the CASE id (`GET /v1/chargebacks/{id}`), so the
|
|
375
|
+
// payment is read from `data.payment_id`; without it there is no payment to dispute
|
|
376
|
+
// and the event stays the `payment.updated` above rather than filing one under a case
|
|
377
|
+
// id that matches nothing.
|
|
378
|
+
if (type === 'topic_chargebacks_wh') {
|
|
379
|
+
const paymentId = payload.data?.payment_id;
|
|
380
|
+
if (paymentId === undefined)
|
|
381
|
+
return event;
|
|
382
|
+
return this.#withChargebackDetails(await this.#paymentEvent(event, String(paymentId)), {
|
|
383
|
+
caseId: resourceId,
|
|
384
|
+
});
|
|
385
|
+
}
|
|
386
|
+
if (type === 'subscription_preapproval') {
|
|
387
|
+
const preapproval = await this.#request(`/preapproval/${resourceId}`);
|
|
388
|
+
const subscription = this.#mapSubscription(preapproval);
|
|
389
|
+
return {
|
|
390
|
+
...event,
|
|
391
|
+
type: preapproval.status === 'canceled' ? 'subscription.canceled' : 'subscription.updated',
|
|
392
|
+
data: {
|
|
393
|
+
gatewayId: subscription.gatewayId,
|
|
394
|
+
customerId: subscription.customerId,
|
|
395
|
+
status: subscription.status,
|
|
396
|
+
planId: subscription.planId,
|
|
397
|
+
...(preapproval.external_reference !== undefined
|
|
398
|
+
? { externalReference: preapproval.external_reference }
|
|
399
|
+
: {}),
|
|
400
|
+
},
|
|
401
|
+
};
|
|
402
|
+
}
|
|
403
|
+
// `subscription_authorized_payment` (a subscription's recurring charge) is left as
|
|
404
|
+
// `payment.updated` with its id: it is read from `/authorized_payments/{id}`, whose
|
|
405
|
+
// response shape this driver has not verified against the reference. Guessing the
|
|
406
|
+
// field names of a settled charge is exactly the mistake that ships wrong money.
|
|
407
|
+
return event;
|
|
408
|
+
}
|
|
409
|
+
/**
|
|
410
|
+
* Fetch a payment and turn it into the webhook event for whatever happened to it.
|
|
411
|
+
*
|
|
412
|
+
* Shared by the `payment` topic (where the notification names the payment) and the
|
|
413
|
+
* chargeback topic (where it names the case and the payment separately), so a dispute
|
|
414
|
+
* is normalized through exactly the same status mapping as everything else.
|
|
415
|
+
*/
|
|
416
|
+
async #paymentEvent(event, paymentId) {
|
|
417
|
+
const payment = await this.#request(`/v1/payments/${paymentId}`);
|
|
418
|
+
const currency = payment.currency_id?.toLowerCase() ?? this.#currency;
|
|
419
|
+
const type = this.#paymentEventType(payment.status, payment.status_detail);
|
|
420
|
+
const outcome = type === 'payment.dispute_closed' ? this.#disputeOutcome(payment.status_detail) : undefined;
|
|
421
|
+
return {
|
|
422
|
+
...event,
|
|
423
|
+
type,
|
|
424
|
+
data: {
|
|
425
|
+
gatewayId: String(payment.id),
|
|
426
|
+
amount: this.#fromAmount(payment.transaction_amount, currency),
|
|
427
|
+
currency,
|
|
428
|
+
...(payment.payer?.id !== undefined ? { customerId: String(payment.payer.id) } : {}),
|
|
429
|
+
...(payment.external_reference !== undefined
|
|
430
|
+
? { externalReference: payment.external_reference }
|
|
431
|
+
: {}),
|
|
432
|
+
// The close has to carry its result: the processor throws on a
|
|
433
|
+
// `payment.dispute_closed` with no outcome rather than inventing one.
|
|
434
|
+
...(outcome !== undefined ? { outcome } : {}),
|
|
435
|
+
},
|
|
436
|
+
};
|
|
437
|
+
}
|
|
438
|
+
/**
|
|
439
|
+
* Add the chargeback CASE's own fields to a dispute event.
|
|
440
|
+
*
|
|
441
|
+
* Only `topic_chargebacks_wh` reaches this, because only that notification names a case
|
|
442
|
+
* (`data.id`). The extra `GET /v1/chargebacks/{id}` buys `date_documentation_deadline` —
|
|
443
|
+
* the evidence deadline, and the single field that makes a Mercado Pago dispute
|
|
444
|
+
* actionable — plus the case's `reason`.
|
|
445
|
+
*
|
|
446
|
+
* It **fails soft**, unlike the payment fetch. By the time this runs the money question
|
|
447
|
+
* is already answered from the payment, and throwing here would turn a chargeback the
|
|
448
|
+
* driver read correctly into a 400 and a redelivery. A missing deadline costs an
|
|
449
|
+
* operator context; a dropped chargeback costs the row.
|
|
450
|
+
*/
|
|
451
|
+
async #withChargebackDetails(event, { caseId }) {
|
|
452
|
+
if (event.type !== 'payment.disputed' && event.type !== 'payment.dispute_closed')
|
|
453
|
+
return event;
|
|
454
|
+
const data = { ...event.data, disputeId: caseId };
|
|
455
|
+
let chargeback;
|
|
456
|
+
try {
|
|
457
|
+
chargeback = await this.#request(`/v1/chargebacks/${caseId}`);
|
|
458
|
+
}
|
|
459
|
+
catch {
|
|
460
|
+
return { ...event, data };
|
|
461
|
+
}
|
|
462
|
+
const deadline = chargeback.date_documentation_deadline;
|
|
463
|
+
return {
|
|
464
|
+
...event,
|
|
465
|
+
data: {
|
|
466
|
+
...data,
|
|
467
|
+
...(typeof chargeback.reason === 'string' && chargeback.reason !== ''
|
|
468
|
+
? { reason: chargeback.reason }
|
|
469
|
+
: {}),
|
|
470
|
+
// ISO 8601 with an offset, e.g. `2024-10-24T12:48:24.000-04:00`. `null` whenever
|
|
471
|
+
// `documentation_required` is false — there is nothing to defend, so no clock.
|
|
472
|
+
...(typeof deadline === 'string' && deadline !== '' ? { actionableUntil: deadline } : {}),
|
|
473
|
+
},
|
|
474
|
+
};
|
|
475
|
+
}
|
|
476
|
+
// ── Mappers ──────────────────────────────────────────────────────────────────────────
|
|
477
|
+
/**
|
|
478
|
+
* Check the `x-signature` HMAC and return the parsed notification.
|
|
479
|
+
*
|
|
480
|
+
* The manifest Mercado Pago signs is `id:<data.id>;request-id:<x-request-id>;ts:<ts>;`,
|
|
481
|
+
* with any absent part left out. The `id` in it is the `data.id` **query parameter**,
|
|
482
|
+
* which `parseWebhook` never sees — the driver contract passes the body and the headers
|
|
483
|
+
* and nothing else — so the same id is read from the body, where Mercado Pago puts it
|
|
484
|
+
* too. The docs say to lowercase an alphanumeric id while the official SDK hashes
|
|
485
|
+
* whatever it is handed, so both spellings are accepted.
|
|
486
|
+
*/
|
|
487
|
+
#verified(rawBody, headers) {
|
|
488
|
+
const payload = JSON.parse(rawBody);
|
|
489
|
+
if (this.#webhookSecret === undefined)
|
|
490
|
+
return payload;
|
|
491
|
+
const header = headerValue(headers, 'x-signature');
|
|
492
|
+
if (header === undefined || header === '') {
|
|
493
|
+
throw new Error('[payments] Missing `x-signature` header on Mercado Pago webhook request.');
|
|
494
|
+
}
|
|
495
|
+
let timestamp;
|
|
496
|
+
let received;
|
|
497
|
+
for (const part of header.split(',')) {
|
|
498
|
+
const separator = part.indexOf('=');
|
|
499
|
+
if (separator === -1)
|
|
500
|
+
continue;
|
|
501
|
+
const key = part.slice(0, separator).trim().toLowerCase();
|
|
502
|
+
const value = part.slice(separator + 1).trim();
|
|
503
|
+
if (key === 'ts')
|
|
504
|
+
timestamp = value;
|
|
505
|
+
else if (key === 'v1')
|
|
506
|
+
received = value;
|
|
507
|
+
}
|
|
508
|
+
if (timestamp === undefined || received === undefined) {
|
|
509
|
+
throw new Error('[payments] Malformed `x-signature` header on Mercado Pago webhook.');
|
|
510
|
+
}
|
|
511
|
+
const requestId = headerValue(headers, 'x-request-id');
|
|
512
|
+
const dataId = payload.data?.id === undefined ? undefined : String(payload.data.id);
|
|
513
|
+
const candidates = dataId !== undefined && dataId !== dataId.toLowerCase()
|
|
514
|
+
? [dataId.toLowerCase(), dataId]
|
|
515
|
+
: [dataId];
|
|
516
|
+
const matched = candidates.some((candidate) => safeCompare(received, this.#signature(candidate, requestId, timestamp)));
|
|
517
|
+
if (!matched) {
|
|
518
|
+
throw new Error('[payments] Invalid Mercado Pago webhook signature.');
|
|
519
|
+
}
|
|
520
|
+
return payload;
|
|
521
|
+
}
|
|
522
|
+
#signature(dataId, requestId, timestamp) {
|
|
523
|
+
const parts = [];
|
|
524
|
+
if (dataId !== undefined && dataId !== '')
|
|
525
|
+
parts.push(`id:${dataId}`);
|
|
526
|
+
if (requestId !== undefined && requestId !== '')
|
|
527
|
+
parts.push(`request-id:${requestId}`);
|
|
528
|
+
parts.push(`ts:${timestamp}`);
|
|
529
|
+
return createHmac('sha256', this.#webhookSecret ?? '')
|
|
530
|
+
.update(`${parts.join(';')};`)
|
|
531
|
+
.digest('hex');
|
|
532
|
+
}
|
|
533
|
+
#mapWebhookType(type) {
|
|
534
|
+
switch (type) {
|
|
535
|
+
// Both carry a payment id and neither says what happened to it.
|
|
536
|
+
case 'payment':
|
|
537
|
+
case 'subscription_authorized_payment':
|
|
538
|
+
return 'payment.updated';
|
|
539
|
+
case 'subscription_preapproval':
|
|
540
|
+
return 'subscription.updated';
|
|
541
|
+
// Deliberately not `payment.disputed`: the notification carries the chargeback case
|
|
542
|
+
// id, not the payment id, and the only honest dispute event is the one built after
|
|
543
|
+
// the payment named by `data.payment_id` has been fetched. This is what the topic
|
|
544
|
+
// degrades to when that id is missing.
|
|
545
|
+
case 'topic_chargebacks_wh':
|
|
546
|
+
return 'payment.updated';
|
|
547
|
+
default:
|
|
548
|
+
return type === '' ? 'unknown' : type.toLowerCase();
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
#paymentEventType(status, statusDetail) {
|
|
552
|
+
switch (status) {
|
|
553
|
+
case 'approved':
|
|
554
|
+
return 'payment.succeeded';
|
|
555
|
+
case 'rejected':
|
|
556
|
+
case 'cancelled':
|
|
557
|
+
return 'payment.failed';
|
|
558
|
+
case 'refunded':
|
|
559
|
+
return 'payment.refunded';
|
|
560
|
+
// A card chargeback, and `status_detail` says where it stands. Mercado Pago's own
|
|
561
|
+
// table: `in_process` is "chargeback received, the dispute is in progress, awaiting a
|
|
562
|
+
// final decision"; `settled` is "decision against the seller, money withdrawn from
|
|
563
|
+
// the seller's account"; `reimbursed` is "decision in favor of the seller, money
|
|
564
|
+
// refunded to the seller's account". So the two terminal details are the outcome, and
|
|
565
|
+
// reading them is the difference between a dispute that closes and one that sits at
|
|
566
|
+
// `disputed` forever — a WON dispute left there writes off money that came back,
|
|
567
|
+
// because `revenue()` only sums `paid`.
|
|
568
|
+
//
|
|
569
|
+
// Note what the reference does NOT say: whether the funds leave the account at
|
|
570
|
+
// `in_process` or only at `settled`. Mercado Pago describes `settled` as the
|
|
571
|
+
// withdrawal, but the payment's top-level status is already `charged_back` and the
|
|
572
|
+
// guides describe the amount as retained from the opening. Since the reference will
|
|
573
|
+
// not settle it, this stays `payment.disputed` — the mapping it already had — rather
|
|
574
|
+
// than being downgraded to a warning on a guess.
|
|
575
|
+
case 'charged_back':
|
|
576
|
+
return this.#disputeOutcome(statusDetail) === undefined
|
|
577
|
+
? 'payment.disputed'
|
|
578
|
+
: 'payment.dispute_closed';
|
|
579
|
+
// A claim opened inside Mercado Pago rather than at the issuer. The reference says
|
|
580
|
+
// only "users have initiated a dispute" and does not say whether the money is
|
|
581
|
+
// withheld, so this keeps the `payment.disputed` it has always had; a claim has no
|
|
582
|
+
// documented `status_detail` outcome to close on either.
|
|
583
|
+
case 'in_mediation':
|
|
584
|
+
return 'payment.disputed';
|
|
585
|
+
default:
|
|
586
|
+
return 'payment.updated';
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
/**
|
|
590
|
+
* The outcome of a `charged_back` payment, read from `status_detail`.
|
|
591
|
+
*
|
|
592
|
+
* `in_process` — and anything unrecognized — returns `undefined`, which keeps the event
|
|
593
|
+
* at `payment.disputed`. The dispute is open; there is no result to report, and inventing
|
|
594
|
+
* one would close a row over money still being argued about.
|
|
595
|
+
*
|
|
596
|
+
* `coverage_applied` on the chargeback case says the same thing (`true` favours the
|
|
597
|
+
* seller), but it is on a resource the `payment` topic never names, so the payment's own
|
|
598
|
+
* detail is what this reads.
|
|
599
|
+
*/
|
|
600
|
+
#disputeOutcome(statusDetail) {
|
|
601
|
+
if (statusDetail === 'reimbursed')
|
|
602
|
+
return 'won';
|
|
603
|
+
if (statusDetail === 'settled')
|
|
604
|
+
return 'lost';
|
|
605
|
+
return undefined;
|
|
606
|
+
}
|
|
607
|
+
#mapCustomer(data) {
|
|
608
|
+
const name = [data.first_name, data.last_name].filter(Boolean).join(' ');
|
|
609
|
+
return {
|
|
610
|
+
id: String(data.id),
|
|
611
|
+
...(data.email !== undefined ? { email: data.email } : {}),
|
|
612
|
+
...(name !== '' ? { name } : {}),
|
|
613
|
+
...(data.identification?.number !== undefined ? { taxId: data.identification.number } : {}),
|
|
614
|
+
};
|
|
615
|
+
}
|
|
616
|
+
#mapPayment(data, fallbackCurrency) {
|
|
617
|
+
const statusMap = {
|
|
618
|
+
approved: 'paid',
|
|
619
|
+
// The card was held (`status_detail: pending_capture`) and nothing was captured —
|
|
620
|
+
// the money has NOT moved. It used to collapse into `pending`, which understates a
|
|
621
|
+
// hold the issuer already granted; `paid` would have granted access against money
|
|
622
|
+
// that can still evaporate when the authorization expires uncaptured.
|
|
623
|
+
authorized: 'authorized',
|
|
624
|
+
pending: 'pending',
|
|
625
|
+
in_process: 'pending',
|
|
626
|
+
in_mediation: 'disputed',
|
|
627
|
+
rejected: 'failed',
|
|
628
|
+
cancelled: 'canceled',
|
|
629
|
+
refunded: 'refunded',
|
|
630
|
+
charged_back: 'disputed',
|
|
631
|
+
};
|
|
632
|
+
const currency = data.currency_id?.toLowerCase() ?? fallbackCurrency;
|
|
633
|
+
const method = this.#mapMethodToType(data.payment_method_id, data.payment_type_id);
|
|
634
|
+
const result = {
|
|
635
|
+
id: String(data.id),
|
|
636
|
+
gatewayId: String(data.id),
|
|
637
|
+
provider: this.provider,
|
|
638
|
+
amount: { amount: this.#fromAmount(data.transaction_amount, currency), currency },
|
|
639
|
+
status: statusMap[data.status ?? ''] ?? 'pending',
|
|
640
|
+
payload: data,
|
|
641
|
+
createdAt: data.date_created ?? new Date().toISOString(),
|
|
642
|
+
};
|
|
643
|
+
if (method !== 'unknown')
|
|
644
|
+
result.method = method;
|
|
645
|
+
if (data.payer?.id !== undefined)
|
|
646
|
+
result.customerId = String(data.payer.id);
|
|
647
|
+
if (data.date_approved !== undefined)
|
|
648
|
+
result.paidAt = data.date_approved;
|
|
649
|
+
const pix = data.point_of_interaction?.transaction_data;
|
|
650
|
+
if (pix?.qr_code !== undefined) {
|
|
651
|
+
result.pixCode = pix.qr_code;
|
|
652
|
+
result.pixCopiaECola = pix.qr_code;
|
|
653
|
+
}
|
|
654
|
+
if (pix?.qr_code_base64 !== undefined) {
|
|
655
|
+
result.pixQrCodeImage = pix.qr_code_base64;
|
|
656
|
+
result.pixQrCode = pix.qr_code_base64;
|
|
657
|
+
}
|
|
658
|
+
const hostedUrl = pix?.ticket_url ?? data.transaction_details?.external_resource_url;
|
|
659
|
+
if (hostedUrl !== undefined)
|
|
660
|
+
result.hostedUrl = hostedUrl;
|
|
661
|
+
return result;
|
|
662
|
+
}
|
|
663
|
+
#mapSubscription(data) {
|
|
664
|
+
const statusMap = {
|
|
665
|
+
authorized: 'active',
|
|
666
|
+
// The payer has not authorized it yet — nothing has been charged.
|
|
667
|
+
pending: 'incomplete',
|
|
668
|
+
// Billing has stopped and the preapproval is still alive — it can be resumed, and
|
|
669
|
+
// it must not entitle the payer meanwhile. It used to map to `past_due`, which says
|
|
670
|
+
// a charge failed; nothing failed here, Mercado Pago was told to stop.
|
|
671
|
+
paused: 'paused',
|
|
672
|
+
canceled: 'canceled',
|
|
673
|
+
};
|
|
674
|
+
const currency = data.auto_recurring?.currency_id?.toLowerCase() ?? this.#currency;
|
|
675
|
+
return {
|
|
676
|
+
id: data.id,
|
|
677
|
+
gatewayId: data.id,
|
|
678
|
+
provider: this.provider,
|
|
679
|
+
customerId: data.payer_id === undefined ? '' : String(data.payer_id),
|
|
680
|
+
status: statusMap[data.status ?? ''] ?? 'incomplete',
|
|
681
|
+
planId: data.preapproval_plan_id ?? data.reason ?? '',
|
|
682
|
+
...(data.auto_recurring?.transaction_amount !== undefined
|
|
683
|
+
? {
|
|
684
|
+
amount: {
|
|
685
|
+
amount: this.#fromAmount(data.auto_recurring.transaction_amount, currency),
|
|
686
|
+
currency,
|
|
687
|
+
},
|
|
688
|
+
}
|
|
689
|
+
: {}),
|
|
690
|
+
...(data.auto_recurring?.start_date !== undefined
|
|
691
|
+
? { currentPeriodStart: data.auto_recurring.start_date }
|
|
692
|
+
: {}),
|
|
693
|
+
...(data.next_payment_date !== undefined ? { currentPeriodEnd: data.next_payment_date } : {}),
|
|
694
|
+
...(data.auto_recurring?.end_date !== undefined
|
|
695
|
+
? { endsAt: data.auto_recurring.end_date }
|
|
696
|
+
: {}),
|
|
697
|
+
payload: data,
|
|
698
|
+
createdAt: data.date_created ?? new Date().toISOString(),
|
|
699
|
+
};
|
|
700
|
+
}
|
|
701
|
+
/** `WEEKLY` and friends onto the only two frequency types Mercado Pago accepts. */
|
|
702
|
+
#recurrence(cycle) {
|
|
703
|
+
switch (cycle) {
|
|
704
|
+
case 'WEEKLY':
|
|
705
|
+
return { frequency: 7, frequency_type: 'days' };
|
|
706
|
+
case 'BIWEEKLY':
|
|
707
|
+
return { frequency: 14, frequency_type: 'days' };
|
|
708
|
+
case 'QUARTERLY':
|
|
709
|
+
return { frequency: 3, frequency_type: 'months' };
|
|
710
|
+
case 'SEMIANNUALLY':
|
|
711
|
+
return { frequency: 6, frequency_type: 'months' };
|
|
712
|
+
case 'YEARLY':
|
|
713
|
+
return { frequency: 12, frequency_type: 'months' };
|
|
714
|
+
default:
|
|
715
|
+
return { frequency: 1, frequency_type: 'months' };
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
/**
|
|
719
|
+
* The `payment_method_id` for this charge. Pix and boleto have fixed ids; a card is
|
|
720
|
+
* identified by its brand (`visa`, `master`, `debvisa`…), which the tokenizer in the
|
|
721
|
+
* frontend returns alongside the token and only the caller can know.
|
|
722
|
+
*/
|
|
723
|
+
#paymentMethodId(input, hasToken) {
|
|
724
|
+
if (typeof input.metadata?.paymentMethodId === 'string')
|
|
725
|
+
return input.metadata.paymentMethodId;
|
|
726
|
+
switch (input.method) {
|
|
727
|
+
case 'pix':
|
|
728
|
+
return 'pix';
|
|
729
|
+
case 'boleto':
|
|
730
|
+
return 'bolbradesco';
|
|
731
|
+
case 'credit_card':
|
|
732
|
+
case 'debit_card':
|
|
733
|
+
throw new Error('[payments] Mercado Pago identifies a card payment by its brand, not by "card". ' +
|
|
734
|
+
'Pass the `payment_method_id` the card tokenizer returned (e.g. "visa", "master", ' +
|
|
735
|
+
'"debvisa") as `metadata.paymentMethodId`.');
|
|
736
|
+
default:
|
|
737
|
+
throw new Error(hasToken
|
|
738
|
+
? '[payments] Mercado Pago needs the card brand for a tokenized charge — pass it ' +
|
|
739
|
+
'as `metadata.paymentMethodId` (e.g. "visa").'
|
|
740
|
+
: '[payments] Mercado Pago requires a payment method on every charge. Pass ' +
|
|
741
|
+
'`method: "pix"` or `"boleto"`, or use `createCheckout()` to let the payer ' +
|
|
742
|
+
'choose on Checkout Pro.');
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
#payer(input) {
|
|
746
|
+
const holder = input.card?.holder;
|
|
747
|
+
const email = input.customer?.email ?? holder?.email;
|
|
748
|
+
const name = input.customer?.name ?? holder?.name;
|
|
749
|
+
const taxId = input.customer?.taxId ?? holder?.cpfCnpj;
|
|
750
|
+
const [firstName, ...rest] = (name ?? '').split(' ');
|
|
751
|
+
return {
|
|
752
|
+
...(input.customerId !== undefined ? { id: input.customerId, type: 'customer' } : {}),
|
|
753
|
+
...(email !== undefined ? { email } : {}),
|
|
754
|
+
...(firstName !== undefined && firstName !== '' ? { first_name: firstName } : {}),
|
|
755
|
+
...(rest.length > 0 ? { last_name: rest.join(' ') } : {}),
|
|
756
|
+
...(taxId !== undefined ? { identification: this.#identification(taxId) } : {}),
|
|
757
|
+
// The reference documents the payer address under `additional_info.payer.address`
|
|
758
|
+
// while the Pix and boleto guides nest it here; boleto rejects a payment without it.
|
|
759
|
+
// The guides are the ones with working end-to-end examples.
|
|
760
|
+
...(typeof input.metadata?.address === 'object' && input.metadata.address !== null
|
|
761
|
+
? { address: input.metadata.address }
|
|
762
|
+
: {}),
|
|
763
|
+
};
|
|
764
|
+
}
|
|
765
|
+
/**
|
|
766
|
+
* A tax id and its Mercado Pago `identification.type`. Only Brazil's CPF/CNPJ can be
|
|
767
|
+
* told apart by length; every other country has its own document types, so those must
|
|
768
|
+
* be passed explicitly.
|
|
769
|
+
*/
|
|
770
|
+
#identification(taxId) {
|
|
771
|
+
const digits = taxId.replace(/\D/g, '');
|
|
772
|
+
return { type: digits.length > 11 ? 'CNPJ' : 'CPF', number: digits };
|
|
773
|
+
}
|
|
774
|
+
#assertExternalReference(value) {
|
|
775
|
+
if (!EXTERNAL_REFERENCE_PATTERN.test(value)) {
|
|
776
|
+
throw new Error(`[payments] Mercado Pago rejects the externalReference "${value}". It allows at most ` +
|
|
777
|
+
`64 characters — letters, numbers, hyphens and underscores — and got ${value.length}.`);
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
#mapMethodToType(paymentMethodId, paymentTypeId) {
|
|
781
|
+
if (paymentMethodId?.toLowerCase() === 'pix')
|
|
782
|
+
return 'pix';
|
|
783
|
+
switch (paymentTypeId) {
|
|
784
|
+
case 'credit_card':
|
|
785
|
+
case 'prepaid_card':
|
|
786
|
+
return 'card';
|
|
787
|
+
case 'debit_card':
|
|
788
|
+
return 'debit_card';
|
|
789
|
+
case 'ticket':
|
|
790
|
+
return 'boleto';
|
|
791
|
+
default:
|
|
792
|
+
return 'unknown';
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
/**
|
|
796
|
+
* `X-Idempotency-Key` is documented — and mandatory — on `POST /v1/payments` and
|
|
797
|
+
* `POST /v1/payments/{id}/refunds`, and on nothing else: the reference for
|
|
798
|
+
* `/v1/customers` and `/preapproval` lists `Authorization` and no more. Sending it
|
|
799
|
+
* there anyway would be harmless and unspecified, which is the worst combination — the
|
|
800
|
+
* caller would believe their retry is safe. So those operations refuse it.
|
|
801
|
+
*/
|
|
802
|
+
#refuseIdempotencyKey(key, operation, endpoint) {
|
|
803
|
+
if (key === undefined)
|
|
804
|
+
return;
|
|
805
|
+
throw new Error(`[payments] Mercado Pago documents \`X-Idempotency-Key\` only for payments and refunds, so \`${operation}\` cannot honour an idempotencyKey — its \`${endpoint}\` reference documents no such header. \`charge()\` and \`refund()\` do honour it; for the rest, deduplicate before you call (a preapproval can be looked up by \`external_reference\`).`);
|
|
806
|
+
}
|
|
807
|
+
#numberOption(value) {
|
|
808
|
+
return typeof value === 'number' ? value : undefined;
|
|
809
|
+
}
|
|
810
|
+
// ── Money ────────────────────────────────────────────────────────────────────────────
|
|
811
|
+
/**
|
|
812
|
+
* Minor units → the decimal `transaction_amount` Mercado Pago wants (a JSON number, not
|
|
813
|
+
* a string). The currency has to travel with it: CLP has no cents, so dividing a
|
|
814
|
+
* Chilean amount by 100 bills 1% of it — and Mercado Pago accepts that happily.
|
|
815
|
+
*
|
|
816
|
+
* COP is not a special case here: ISO 4217 gives it two minor units even though
|
|
817
|
+
* Colombian prices are quoted in whole pesos. Check a COP charge in sandbox.
|
|
818
|
+
*/
|
|
819
|
+
#toAmount(amount, currency) {
|
|
820
|
+
return toDecimal(amount, currency);
|
|
821
|
+
}
|
|
822
|
+
/** Mercado Pago's decimal amount → minor units. It sometimes quotes them as strings. */
|
|
823
|
+
#fromAmount(value, currency) {
|
|
824
|
+
const parsed = typeof value === 'string' ? Number(value) : (value ?? 0);
|
|
825
|
+
if (Number.isNaN(parsed))
|
|
826
|
+
return 0;
|
|
827
|
+
return fromDecimal(parsed, currency);
|
|
828
|
+
}
|
|
829
|
+
// ── Transport ────────────────────────────────────────────────────────────────────────
|
|
830
|
+
async #request(path, options = {}) {
|
|
831
|
+
return httpRequest(path, {
|
|
832
|
+
baseUrl: this.#baseUrl,
|
|
833
|
+
bearerToken: this.#accessToken,
|
|
834
|
+
...(options.method !== undefined ? { method: options.method } : {}),
|
|
835
|
+
...(options.body !== undefined ? { body: options.body } : {}),
|
|
836
|
+
// Mercado Pago's idempotency key is a header — written into the body it would
|
|
837
|
+
// deduplicate nothing.
|
|
838
|
+
...(options.idempotencyKey !== undefined
|
|
839
|
+
? { headers: { 'X-Idempotency-Key': options.idempotencyKey } }
|
|
840
|
+
: {}),
|
|
841
|
+
});
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
//# sourceMappingURL=mercadopago.js.map
|