@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,628 @@
|
|
|
1
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
2
|
+
import { readFileSync } from 'node:fs';
|
|
3
|
+
import { Agent as HttpsAgent, request as httpsRequest } from 'node:https';
|
|
4
|
+
import { publishPaymentDiagnostics, publishRefundDiagnostics } from '../diagnostics.js';
|
|
5
|
+
import { httpRequest, isNotFound } from '../http.js';
|
|
6
|
+
import { emitInvoiceIfRequested } from '../invoice/emit_invoice.js';
|
|
7
|
+
import { formatDecimal, fromDecimal } from '../money.js';
|
|
8
|
+
import { requireCredential } from './shared.js';
|
|
9
|
+
/**
|
|
10
|
+
* A `devolução` executed under BACEN's MED (Mecanismo Especial de Devolução) rather than
|
|
11
|
+
* asked for by the merchant. The API Pix `natureza` enum has three of them —
|
|
12
|
+
* `MED_OPERACIONAL` (operational failure), `MED_FRAUDE` (founded suspicion of fraud) and
|
|
13
|
+
* `MED_PIX_AUTOMATICO` — and every other value (`ORIGINAL`, `RETIRADA`, or nothing) is a
|
|
14
|
+
* refund the merchant chose to make.
|
|
15
|
+
*/
|
|
16
|
+
function isMedDevolucao(devolucao) {
|
|
17
|
+
return devolucao.natureza?.startsWith('MED') === true;
|
|
18
|
+
}
|
|
19
|
+
/** Renew a token this many seconds before it actually expires. */
|
|
20
|
+
const TOKEN_RENEWAL_SKEW_SECONDS = 60;
|
|
21
|
+
/** What Efí's `/oauth/token` returns today; used only when a response omits `expires_in`. */
|
|
22
|
+
const TOKEN_FALLBACK_TTL_SECONDS = 3600;
|
|
23
|
+
/**
|
|
24
|
+
* Efí (formerly Gerencianet) driver — the **Pix API** (`pix.api.efipay.com.br`).
|
|
25
|
+
*
|
|
26
|
+
* Efí sells several APIs behind one brand and they do not share an auth model. This driver
|
|
27
|
+
* speaks the **Pix API** only: immediate charges (`cob`), the Pix that settle them and
|
|
28
|
+
* their refunds (`devolução`). The **Cobranças API** (`cobrancas.api.efipay.com.br`) is a
|
|
29
|
+
* different product — boleto, card, carnê and native subscriptions, OAuth **without** a
|
|
30
|
+
* certificate — and none of it is reachable from here. If you need boleto or recurring
|
|
31
|
+
* billing from Efí, that is a second driver, not a config flag.
|
|
32
|
+
*
|
|
33
|
+
* Two consequences worth knowing before you configure it:
|
|
34
|
+
*
|
|
35
|
+
* - **Mutual TLS is mandatory**, including on the token request. A client certificate is a
|
|
36
|
+
* property of the TLS connection, not a header, so it cannot be handed to `httpRequest`
|
|
37
|
+
* the way an API key can. The driver builds its own certificate-bearing `fetch` over
|
|
38
|
+
* `node:https` and passes it through the helper's `fetch` seam — no extra dependency,
|
|
39
|
+
* but you must supply the `.p12` Efí generated for you, or your own `fetch`.
|
|
40
|
+
* - **The access token expires** (an hour today). It is cached against the `expires_in`
|
|
41
|
+
* the gateway actually returned, minus a minute, so the cache can never outlive the
|
|
42
|
+
* token; a 401 also drops it and retries once.
|
|
43
|
+
*/
|
|
44
|
+
export class EfiDriver {
|
|
45
|
+
provider = 'efi';
|
|
46
|
+
// Pix only. Not `'undefined'` either: there is no "payer picks a method" on this API.
|
|
47
|
+
supportedMethods = ['pix'];
|
|
48
|
+
capabilities = { refunds: true, invoices: false, subscriptions: false };
|
|
49
|
+
#baseUrl;
|
|
50
|
+
#basicAuth;
|
|
51
|
+
#pixKey;
|
|
52
|
+
#fetch;
|
|
53
|
+
#invoiceCtx;
|
|
54
|
+
#expirationSeconds;
|
|
55
|
+
/** Cached OAuth token. `expiresAt` is an epoch ms deadline, never a fixed lifetime. */
|
|
56
|
+
#token;
|
|
57
|
+
/** In-flight token request, so concurrent charges don't each mint a token. */
|
|
58
|
+
#tokenInFlight;
|
|
59
|
+
constructor(ctx, config = {}) {
|
|
60
|
+
this.#invoiceCtx = ctx;
|
|
61
|
+
const clientId = requireCredential({
|
|
62
|
+
driver: 'efi',
|
|
63
|
+
option: 'clientId',
|
|
64
|
+
env: 'EFI_CLIENT_ID',
|
|
65
|
+
value: config.clientId,
|
|
66
|
+
});
|
|
67
|
+
const clientSecret = requireCredential({
|
|
68
|
+
driver: 'efi',
|
|
69
|
+
option: 'clientSecret',
|
|
70
|
+
env: 'EFI_CLIENT_SECRET',
|
|
71
|
+
value: config.clientSecret,
|
|
72
|
+
});
|
|
73
|
+
// Not a credential, but the same rule applies: the Pix API refuses a `cob` without the
|
|
74
|
+
// key that receives it, and boot is the last honest place to notice.
|
|
75
|
+
this.#pixKey = requireCredential({
|
|
76
|
+
driver: 'efi',
|
|
77
|
+
option: 'pixKey',
|
|
78
|
+
env: 'EFI_PIX_KEY',
|
|
79
|
+
value: config.pixKey,
|
|
80
|
+
});
|
|
81
|
+
this.#basicAuth = Buffer.from(`${clientId}:${clientSecret}`).toString('base64');
|
|
82
|
+
const sandbox = config.sandbox ?? process.env.NODE_ENV !== 'production';
|
|
83
|
+
this.#baseUrl = sandbox ? 'https://pix-h.api.efipay.com.br' : 'https://pix.api.efipay.com.br';
|
|
84
|
+
this.#expirationSeconds = config.expirationSeconds ?? 3600;
|
|
85
|
+
this.#fetch = config.fetch ?? mtlsFetch(config);
|
|
86
|
+
}
|
|
87
|
+
// ── Customers ────────────────────────────────────────────────────────────────────────
|
|
88
|
+
async createCustomer(_input) {
|
|
89
|
+
throw new Error("[payments] Efí's Pix API has no customer resource — the payer (`devedor`) is sent inline on each charge. " +
|
|
90
|
+
'Pass `customer: { name, taxId }` on the charge and keep the mapping in your own table.');
|
|
91
|
+
}
|
|
92
|
+
async findCustomer(_customerId) {
|
|
93
|
+
throw new Error("[payments] Efí's Pix API has no customer resource to look up — the payer lives on the charge.");
|
|
94
|
+
}
|
|
95
|
+
async updateCustomer(_customerId, _input) {
|
|
96
|
+
throw new Error("[payments] Efí's Pix API has no customer resource to update — the payer is sent with every charge.");
|
|
97
|
+
}
|
|
98
|
+
// ── Payments ─────────────────────────────────────────────────────────────────────────
|
|
99
|
+
async charge(input) {
|
|
100
|
+
if (input.method !== undefined && input.method !== 'pix') {
|
|
101
|
+
throw new Error(`[payments] Efí's Pix API only settles Pix; this driver cannot create a "${input.method}" charge. Boleto and card are the Cobranças API, a different Efí product.`);
|
|
102
|
+
}
|
|
103
|
+
const payer = this.#payer(input);
|
|
104
|
+
const body = {
|
|
105
|
+
calendario: { expiracao: this.#expiration(input) },
|
|
106
|
+
valor: { original: toAmountString(input.amount) },
|
|
107
|
+
chave: this.#pixKey,
|
|
108
|
+
...(payer !== undefined ? { devedor: payer } : {}),
|
|
109
|
+
...(input.description !== undefined
|
|
110
|
+
? { solicitacaoPagador: input.description.slice(0, 140) }
|
|
111
|
+
: {}),
|
|
112
|
+
};
|
|
113
|
+
// The txid is the ONLY id Efí echoes on the webhook, so when the app's reference fits
|
|
114
|
+
// the txid charset it becomes the txid — and the webhook routes itself. When it does
|
|
115
|
+
// not fit, Efí generates one and the app must persist the returned `gatewayId`.
|
|
116
|
+
const reference = input.externalReference ?? input.idempotencyKey;
|
|
117
|
+
const cob = reference !== undefined && isValidTxid(reference)
|
|
118
|
+
? await this.#request(`/v2/cob/${reference}`, { method: 'PUT', body })
|
|
119
|
+
: await this.#request('/v2/cob', { method: 'POST', body });
|
|
120
|
+
const payment = this.#mapCob(cob);
|
|
121
|
+
await this.#attachQrCode(payment, cob);
|
|
122
|
+
await emitInvoiceIfRequested(this.#invoiceCtx, input, payment, this);
|
|
123
|
+
publishPaymentDiagnostics(payment);
|
|
124
|
+
return payment;
|
|
125
|
+
}
|
|
126
|
+
async findPayment(gatewayId) {
|
|
127
|
+
try {
|
|
128
|
+
const cob = await this.#request(`/v2/cob/${gatewayId}`);
|
|
129
|
+
return this.#mapCob(cob);
|
|
130
|
+
}
|
|
131
|
+
catch (error) {
|
|
132
|
+
if (isNotFound(error))
|
|
133
|
+
return null;
|
|
134
|
+
throw error;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* `options.idempotencyKey` becomes the **devolução id** — Efí's refund is a `PUT` to
|
|
139
|
+
* `/v2/pix/{e2eid}/devolucao/{id}`, so the id you choose *is* the deduplication: a retry
|
|
140
|
+
* with the same one returns the first refund instead of sending the money twice. Without
|
|
141
|
+
* a key the driver mints a random id, which protects a retry inside one call and nothing
|
|
142
|
+
* across process restarts, so pass one if you retry.
|
|
143
|
+
*/
|
|
144
|
+
async refund(paymentGatewayId, amount, options) {
|
|
145
|
+
// A Pix refund is made against the settled Pix (its endToEndId), not against the
|
|
146
|
+
// charge — so a txid has to be resolved to the Pix that paid it first.
|
|
147
|
+
const { endToEndId, paidValue } = await this.#resolveSettledPix(paymentGatewayId);
|
|
148
|
+
const value = amount !== undefined ? toAmountString(amount) : paidValue;
|
|
149
|
+
// Efí requires an id for the refund itself (max 35 chars, alphanumeric); it is also
|
|
150
|
+
// the idempotency key on their side, so a retry with the same id is not a second refund.
|
|
151
|
+
const refundId = idempotencyKeyAsRefundId(options?.idempotencyKey) ?? randomUUID().replace(/-/g, '');
|
|
152
|
+
const data = await this.#request(`/v2/pix/${endToEndId}/devolucao/${refundId}`, {
|
|
153
|
+
method: 'PUT',
|
|
154
|
+
body: { valor: value },
|
|
155
|
+
});
|
|
156
|
+
const refund = {
|
|
157
|
+
id: data.id ?? refundId,
|
|
158
|
+
gatewayId: data.rtrId ?? data.id ?? refundId,
|
|
159
|
+
provider: this.provider,
|
|
160
|
+
amount: { amount: fromDecimal(Number(data.valor ?? value)), currency: 'brl' },
|
|
161
|
+
status: data.status === 'DEVOLVIDO'
|
|
162
|
+
? 'succeeded'
|
|
163
|
+
: data.status === 'NAO_REALIZADO'
|
|
164
|
+
? 'failed'
|
|
165
|
+
: 'pending',
|
|
166
|
+
createdAt: data.horario?.solicitacao ?? new Date().toISOString(),
|
|
167
|
+
};
|
|
168
|
+
publishRefundDiagnostics(refund);
|
|
169
|
+
return refund;
|
|
170
|
+
}
|
|
171
|
+
// ── Checkout ─────────────────────────────────────────────────────────────────────────
|
|
172
|
+
async createCheckout(input) {
|
|
173
|
+
// There is no hosted page: a Pix charge is a BR Code the payer copies or scans, so
|
|
174
|
+
// `url` stays empty rather than pointing at something Efí does not host.
|
|
175
|
+
const body = {
|
|
176
|
+
calendario: { expiracao: this.#expirationSeconds },
|
|
177
|
+
valor: { original: toAmountString(input.amount) },
|
|
178
|
+
chave: this.#pixKey,
|
|
179
|
+
...(input.description !== undefined
|
|
180
|
+
? { solicitacaoPagador: input.description.slice(0, 140) }
|
|
181
|
+
: {}),
|
|
182
|
+
};
|
|
183
|
+
// The txid is the only reference Efí echoes on the webhook, so a checkout reference
|
|
184
|
+
// that fits the txid charset becomes the txid — exactly as on a charge.
|
|
185
|
+
const reference = input.externalReference ?? input.idempotencyKey;
|
|
186
|
+
const cob = reference !== undefined && isValidTxid(reference)
|
|
187
|
+
? await this.#request(`/v2/cob/${reference}`, { method: 'PUT', body })
|
|
188
|
+
: await this.#request('/v2/cob', { method: 'POST', body });
|
|
189
|
+
return {
|
|
190
|
+
id: cob.txid,
|
|
191
|
+
gatewayId: cob.txid,
|
|
192
|
+
provider: this.provider,
|
|
193
|
+
url: '',
|
|
194
|
+
status: cob.status === 'CONCLUIDA' ? 'complete' : 'open',
|
|
195
|
+
amount: { amount: input.amount, currency: 'brl' },
|
|
196
|
+
...(cob.pixCopiaECola !== undefined
|
|
197
|
+
? { pixCode: cob.pixCopiaECola, pixCopiaECola: cob.pixCopiaECola }
|
|
198
|
+
: {}),
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
// ── Subscriptions ────────────────────────────────────────────────────────────────────
|
|
202
|
+
async createSubscription(_input) {
|
|
203
|
+
throw new Error("[payments] Efí's Pix API has no subscriptions. Recurring billing is either the Cobranças API " +
|
|
204
|
+
'(`/v1/subscription`) or Pix Automático (`/v2/rec`) — both different products with different scopes, ' +
|
|
205
|
+
'neither of which this driver speaks.');
|
|
206
|
+
}
|
|
207
|
+
async cancelSubscription(_subscriptionGatewayId, _options) {
|
|
208
|
+
throw new Error("[payments] Efí's Pix API has no subscriptions to cancel.");
|
|
209
|
+
}
|
|
210
|
+
async updateSubscription(_subscriptionGatewayId, _input) {
|
|
211
|
+
throw new Error("[payments] Efí's Pix API has no subscriptions to update.");
|
|
212
|
+
}
|
|
213
|
+
async findSubscription(_gatewayId) {
|
|
214
|
+
throw new Error("[payments] Efí's Pix API has no subscriptions to look up.");
|
|
215
|
+
}
|
|
216
|
+
// ── Invoices ─────────────────────────────────────────────────────────────────────────
|
|
217
|
+
/**
|
|
218
|
+
* Throws, and deliberately does not return `[]`. An empty list is indistinguishable from
|
|
219
|
+
* "this customer has no invoices", which is the silent shape this package keeps removing:
|
|
220
|
+
* the caller reads zero rows and concludes the customer never bought anything.
|
|
221
|
+
* `capabilities.invoices` is `false`, so `PaymentsManager` already stops the documented
|
|
222
|
+
* path — this message is for whoever reached the driver directly.
|
|
223
|
+
*/
|
|
224
|
+
async listInvoices(_customerId) {
|
|
225
|
+
// Pix charges are indexed by txid and by date range, never by payer — there is no
|
|
226
|
+
// per-customer list to return, and no invoice concept at all.
|
|
227
|
+
throw new Error("[payments] Efí's Pix API has no invoices to list — a Pix charge (`cob`) is indexed " +
|
|
228
|
+
'by txid and by date range, never by payer, and the API has no invoice resource at ' +
|
|
229
|
+
'all. Configure an `invoice` provider and pass `invoice: true` on the charge to ' +
|
|
230
|
+
'emit a nota fiscal, or list charges yourself with `GET /v2/cob` over a date range.');
|
|
231
|
+
}
|
|
232
|
+
// ── Webhooks ─────────────────────────────────────────────────────────────────────────
|
|
233
|
+
/**
|
|
234
|
+
* Point Efí's Pix notifications at `url` for the configured key.
|
|
235
|
+
*
|
|
236
|
+
* Efí appends `/pix` to whatever you register unless the URL ends with `?ignorar=`, and
|
|
237
|
+
* it expects your endpoint to present a certificate (mTLS) unless you say otherwise —
|
|
238
|
+
* hence `skipMtls`, which sets `x-skip-mtls-checking`. When you skip it, the only thing
|
|
239
|
+
* left guarding the endpoint is the `hmac` query parameter you put in `url` and the
|
|
240
|
+
* source IP; see the provider docs page.
|
|
241
|
+
*/
|
|
242
|
+
async registerPixWebhook(url, options = {}) {
|
|
243
|
+
await this.#request(`/v2/webhook/${encodeURIComponent(this.#pixKey)}`, {
|
|
244
|
+
method: 'PUT',
|
|
245
|
+
body: { webhookUrl: url },
|
|
246
|
+
...(options.skipMtls === true ? { headers: { 'x-skip-mtls-checking': 'true' } } : {}),
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
/**
|
|
250
|
+
* Normalize an Efí Pix notification into one event per Pix in the delivery.
|
|
251
|
+
*
|
|
252
|
+
* **Batches.** The body's `pix` key is an array. Efí's reference shows one entry in every
|
|
253
|
+
* example and never states a maximum, and in practice one notification carries one Pix —
|
|
254
|
+
* but the shape is a list, so this reads all of it and returns one {@link WebhookEvent}
|
|
255
|
+
* per entry. A single entry still returns a single event, not an array of one. Efí retries
|
|
256
|
+
* a non-2xx up to 9 times on a progressive backoff, which is what makes the route's
|
|
257
|
+
* "any failure means a non-2xx" answer actually redeliver a Pix that failed to process.
|
|
258
|
+
*/
|
|
259
|
+
/**
|
|
260
|
+
* Whether a delivery to `POST /payments/webhook/:provider` can be authenticated.
|
|
261
|
+
*
|
|
262
|
+
* Efí's Pix callback carries no signature at all: authenticity comes from the mutual-TLS
|
|
263
|
+
* handshake at your edge, or from an `hmac` QUERY parameter this method never sees. There is
|
|
264
|
+
* nothing to configure, so there is nothing for the boot check to demand.
|
|
265
|
+
*/
|
|
266
|
+
get webhookVerification() {
|
|
267
|
+
return 'unsupported';
|
|
268
|
+
}
|
|
269
|
+
parseWebhook(rawBody, _headers) {
|
|
270
|
+
// NOTHING IS VERIFIED HERE, and that is not an oversight. Efí's Pix notification
|
|
271
|
+
// carries no signature header: authenticity is meant to come from the mutual TLS
|
|
272
|
+
// handshake at your edge, or from an `hmac` query parameter — and a query parameter is
|
|
273
|
+
// part of the URL, which this method never sees (the route hands it the body and the
|
|
274
|
+
// headers only). See the Efí provider docs for where to enforce it instead.
|
|
275
|
+
//
|
|
276
|
+
// Note what this means for a batch: with nothing signing the body, there is no per-entry
|
|
277
|
+
// signature to check either, so unlike Adyen there is no per-item verification to get
|
|
278
|
+
// right — the whole delivery is as trusted as the transport that carried it, and no more.
|
|
279
|
+
let payload;
|
|
280
|
+
try {
|
|
281
|
+
payload = JSON.parse(rawBody);
|
|
282
|
+
}
|
|
283
|
+
catch {
|
|
284
|
+
throw new Error('[payments] Efí webhook body is not JSON.');
|
|
285
|
+
}
|
|
286
|
+
if (!('pix' in payload)) {
|
|
287
|
+
// Registering a webhook makes Efí probe the URL with a body that has no `pix`; the
|
|
288
|
+
// registration fails unless the probe gets a 200, so it is passed through as an
|
|
289
|
+
// inert event rather than rejected.
|
|
290
|
+
const name = typeof payload.evento === 'string' ? payload.evento : 'notification';
|
|
291
|
+
return {
|
|
292
|
+
id: `efi-${name}-${createHash('sha256').update(rawBody, 'utf8').digest('hex').slice(0, 24)}`,
|
|
293
|
+
provider: this.provider,
|
|
294
|
+
type: `efi.${name}`,
|
|
295
|
+
createdAt: new Date().toISOString(),
|
|
296
|
+
data: payload,
|
|
297
|
+
raw: payload,
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
const list = payload.pix;
|
|
301
|
+
if (!Array.isArray(list) || list.length === 0) {
|
|
302
|
+
throw new Error('[payments] Efí webhook has an empty `pix` array — nothing to process.');
|
|
303
|
+
}
|
|
304
|
+
// One event per Pix, each keyed on its own endToEndId, so the ledger gives each one its
|
|
305
|
+
// own row: a redelivery of the batch re-runs only the entries that have not been
|
|
306
|
+
// processed, and a batch where one entry fails does not re-grant the ones that did not.
|
|
307
|
+
const events = list.map((pix) => this.#mapPixNotification(pix, payload));
|
|
308
|
+
return events.length === 1 ? events[0] : events;
|
|
309
|
+
}
|
|
310
|
+
/** One entry of the `pix` array → one normalized event. */
|
|
311
|
+
#mapPixNotification(pix, payload) {
|
|
312
|
+
// The envelope narrowed to THIS Pix rather than the whole delivery: the ledger stores
|
|
313
|
+
// `raw` per event, and a row holding the whole batch could not say which entry it is
|
|
314
|
+
// about. Still a valid Efí notification body, and identical to the delivery for a
|
|
315
|
+
// single entry.
|
|
316
|
+
const raw = { ...payload, pix: [pix] };
|
|
317
|
+
const devolucoes = pix.devolucoes ?? [];
|
|
318
|
+
// A Pix cannot be charged back — but it CAN be taken back. The Banco Central's MED
|
|
319
|
+
// returns money to a payer who reported fraud or an operational failure, and it reaches
|
|
320
|
+
// the merchant as an ordinary `devolução` on this very notification, marked by its
|
|
321
|
+
// `natureza`. Treating it as a refund said the merchant chose to give the money back;
|
|
322
|
+
// it is the closest thing Pix has to a chargeback, so it is normalized as one.
|
|
323
|
+
const med = devolucoes.find(isMedDevolucao);
|
|
324
|
+
const refunded = devolucoes.some((d) => !isMedDevolucao(d) && d.status === 'DEVOLVIDO');
|
|
325
|
+
const data = {
|
|
326
|
+
// The txid is the charge id this library stored as `gatewayId` — and, when the app
|
|
327
|
+
// gave a txid-shaped `externalReference`, it is that reference too.
|
|
328
|
+
gatewayId: pix.txid ?? pix.endToEndId,
|
|
329
|
+
amount: fromDecimal(Number(pix.valor)),
|
|
330
|
+
currency: 'brl',
|
|
331
|
+
...(pix.txid !== undefined ? { externalReference: pix.txid } : {}),
|
|
332
|
+
metadata: { endToEndId: pix.endToEndId },
|
|
333
|
+
};
|
|
334
|
+
if (med !== undefined && med.status !== 'NAO_REALIZADO') {
|
|
335
|
+
// `DEVOLVIDO` is the money already gone; `EM_PROCESSAMENTO` is the return being
|
|
336
|
+
// executed and is the earliest the merchant hears of it at all — Efí's Pix webhook
|
|
337
|
+
// has no "a MED was opened" notification, so there is no defense window to announce,
|
|
338
|
+
// only notice. A `NAO_REALIZADO` MED took nothing (Efí's own example: insufficient
|
|
339
|
+
// balance) and is deliberately not a dispute event.
|
|
340
|
+
//
|
|
341
|
+
// The event id carries the devolução id and its status: the ledger keys on it, and
|
|
342
|
+
// reusing the Pix's own id would make the DEVOLVIDO that follows an
|
|
343
|
+
// EM_PROCESSAMENTO look like a redelivery and skip it.
|
|
344
|
+
const withdrawn = med.status === 'DEVOLVIDO';
|
|
345
|
+
return {
|
|
346
|
+
id: `${pix.endToEndId}:med:${med.id}:${med.status}`,
|
|
347
|
+
provider: this.provider,
|
|
348
|
+
type: withdrawn ? 'payment.disputed' : 'payment.dispute_warning',
|
|
349
|
+
createdAt: pix.horario ?? new Date().toISOString(),
|
|
350
|
+
data: {
|
|
351
|
+
...data,
|
|
352
|
+
disputeId: med.id,
|
|
353
|
+
// The `natureza` IS the reason, in BACEN's own vocabulary: `MED_FRAUDE` is a
|
|
354
|
+
// founded suspicion of fraud, `MED_OPERACIONAL` an operational failure.
|
|
355
|
+
...(med.natureza !== undefined ? { reason: med.natureza } : {}),
|
|
356
|
+
// No `actionableUntil`: the API Pix devolução carries no deadline field, and by
|
|
357
|
+
// the time a MED reaches the receiver as a devolução the analysis is over.
|
|
358
|
+
},
|
|
359
|
+
raw,
|
|
360
|
+
};
|
|
361
|
+
}
|
|
362
|
+
return {
|
|
363
|
+
// The endToEndId is unique per Pix and stable across redeliveries — a real event id.
|
|
364
|
+
id: refunded ? `${pix.endToEndId}:devolvido` : pix.endToEndId,
|
|
365
|
+
provider: this.provider,
|
|
366
|
+
type: refunded ? 'payment.refunded' : 'payment.succeeded',
|
|
367
|
+
createdAt: pix.horario ?? new Date().toISOString(),
|
|
368
|
+
data,
|
|
369
|
+
raw,
|
|
370
|
+
};
|
|
371
|
+
}
|
|
372
|
+
// ── Mappers ──────────────────────────────────────────────────────────────────────────
|
|
373
|
+
#mapCob(cob) {
|
|
374
|
+
const settled = cob.pix?.[0];
|
|
375
|
+
const returned = (settled?.devolucoes ?? []).filter((d) => d.status === 'DEVOLVIDO');
|
|
376
|
+
// A MED return is not a refund the merchant made — see `parseWebhook`, which normalizes
|
|
377
|
+
// the same event to `payment.disputed`. Reading the charge back has to agree with it,
|
|
378
|
+
// or the driver reports one gateway state two different ways.
|
|
379
|
+
const medded = returned.some(isMedDevolucao);
|
|
380
|
+
const result = {
|
|
381
|
+
id: cob.txid,
|
|
382
|
+
gatewayId: cob.txid,
|
|
383
|
+
provider: this.provider,
|
|
384
|
+
amount: { amount: fromDecimal(Number(cob.valor.original)), currency: 'brl' },
|
|
385
|
+
status: medded
|
|
386
|
+
? 'disputed'
|
|
387
|
+
: returned.length > 0
|
|
388
|
+
? 'refunded'
|
|
389
|
+
: cob.status === 'CONCLUIDA'
|
|
390
|
+
? 'paid'
|
|
391
|
+
: cob.status === 'ATIVA'
|
|
392
|
+
? 'pending'
|
|
393
|
+
: 'canceled',
|
|
394
|
+
method: 'pix',
|
|
395
|
+
payload: cob,
|
|
396
|
+
createdAt: cob.calendario?.criacao ?? new Date().toISOString(),
|
|
397
|
+
};
|
|
398
|
+
if (cob.pixCopiaECola !== undefined) {
|
|
399
|
+
result.pixCode = cob.pixCopiaECola;
|
|
400
|
+
result.pixCopiaECola = cob.pixCopiaECola;
|
|
401
|
+
}
|
|
402
|
+
if (settled?.horario !== undefined)
|
|
403
|
+
result.paidAt = settled.horario;
|
|
404
|
+
return result;
|
|
405
|
+
}
|
|
406
|
+
/**
|
|
407
|
+
* Fetch the rendered QR image for a charge. Best-effort: the charge already exists and
|
|
408
|
+
* `pixCopiaECola` is what the payer actually needs, so a failure here is not a failure.
|
|
409
|
+
*/
|
|
410
|
+
async #attachQrCode(payment, cob) {
|
|
411
|
+
const locId = cob.loc?.id;
|
|
412
|
+
if (locId === undefined)
|
|
413
|
+
return;
|
|
414
|
+
try {
|
|
415
|
+
const qr = await this.#request(`/v2/loc/${locId}/qrcode`);
|
|
416
|
+
if (qr.qrcode !== undefined && payment.pixCode === undefined) {
|
|
417
|
+
payment.pixCode = qr.qrcode;
|
|
418
|
+
payment.pixCopiaECola = qr.qrcode;
|
|
419
|
+
}
|
|
420
|
+
if (qr.imagemQrcode !== undefined) {
|
|
421
|
+
// Efí returns a full data URI; this field is documented as bare base64.
|
|
422
|
+
const base64 = qr.imagemQrcode.replace(/^data:image\/\w+;base64,/, '');
|
|
423
|
+
payment.pixQrCodeImage = base64;
|
|
424
|
+
payment.pixQrCode = base64;
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
catch {
|
|
428
|
+
// The charge is created and payable — the image is a convenience.
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
/** The payer, only when Efí's minimum (name + CPF/CNPJ) is actually available. */
|
|
432
|
+
#payer(input) {
|
|
433
|
+
const name = input.customer?.name ?? input.card?.holder?.name;
|
|
434
|
+
const taxId = (input.customer?.taxId ?? input.card?.holder?.cpfCnpj)?.replace(/\D/g, '');
|
|
435
|
+
if (!name || !taxId)
|
|
436
|
+
return undefined;
|
|
437
|
+
return { nome: name, ...(taxId.length > 11 ? { cnpj: taxId } : { cpf: taxId }) };
|
|
438
|
+
}
|
|
439
|
+
#expiration(input) {
|
|
440
|
+
const explicit = input.metadata?.expirationSeconds;
|
|
441
|
+
return explicit !== undefined ? Number(explicit) : this.#expirationSeconds;
|
|
442
|
+
}
|
|
443
|
+
/** The settled Pix behind a txid (or an endToEndId passed straight through). */
|
|
444
|
+
async #resolveSettledPix(gatewayId) {
|
|
445
|
+
if (isEndToEndId(gatewayId)) {
|
|
446
|
+
const value = await this.#request(`/v2/pix/${gatewayId}`);
|
|
447
|
+
return { endToEndId: gatewayId, paidValue: value.valor ?? '0.00' };
|
|
448
|
+
}
|
|
449
|
+
const cob = await this.#request(`/v2/cob/${gatewayId}`);
|
|
450
|
+
const settled = cob.pix?.[0];
|
|
451
|
+
if (settled === undefined) {
|
|
452
|
+
throw new Error(`[payments] Efí charge ${gatewayId} has not been paid, so there is no Pix to refund.`);
|
|
453
|
+
}
|
|
454
|
+
return { endToEndId: settled.endToEndId, paidValue: settled.valor };
|
|
455
|
+
}
|
|
456
|
+
// ── Auth + transport ─────────────────────────────────────────────────────────────────
|
|
457
|
+
/**
|
|
458
|
+
* A valid access token, minted on demand and cached only for as long as the gateway
|
|
459
|
+
* said it is good for.
|
|
460
|
+
*
|
|
461
|
+
* The deadline comes from the response's own `expires_in`, minus a minute of skew — a
|
|
462
|
+
* cache with a lifetime of its own is how you get a driver that works all afternoon and
|
|
463
|
+
* starts 401-ing an hour into the first deploy that stays up.
|
|
464
|
+
*/
|
|
465
|
+
async #accessToken() {
|
|
466
|
+
const cached = this.#token;
|
|
467
|
+
if (cached !== undefined && cached.expiresAt > Date.now())
|
|
468
|
+
return cached.value;
|
|
469
|
+
// Collapse concurrent misses onto one token request.
|
|
470
|
+
if (this.#tokenInFlight !== undefined)
|
|
471
|
+
return this.#tokenInFlight;
|
|
472
|
+
const request = this.#mintToken().finally(() => {
|
|
473
|
+
this.#tokenInFlight = undefined;
|
|
474
|
+
});
|
|
475
|
+
this.#tokenInFlight = request;
|
|
476
|
+
return request;
|
|
477
|
+
}
|
|
478
|
+
async #mintToken() {
|
|
479
|
+
const data = await httpRequest('/oauth/token', {
|
|
480
|
+
baseUrl: this.#baseUrl,
|
|
481
|
+
method: 'POST',
|
|
482
|
+
body: { grant_type: 'client_credentials' },
|
|
483
|
+
authHeader: { name: 'Authorization', value: `Basic ${this.#basicAuth}` },
|
|
484
|
+
fetch: this.#fetch,
|
|
485
|
+
});
|
|
486
|
+
if (!data.access_token) {
|
|
487
|
+
throw new Error('[payments] Efí returned no access_token for the client-credentials grant.');
|
|
488
|
+
}
|
|
489
|
+
const ttl = Number(data.expires_in);
|
|
490
|
+
const seconds = Number.isFinite(ttl) && ttl > 0 ? ttl : TOKEN_FALLBACK_TTL_SECONDS;
|
|
491
|
+
const skew = Math.min(TOKEN_RENEWAL_SKEW_SECONDS, Math.floor(seconds / 2));
|
|
492
|
+
this.#token = {
|
|
493
|
+
value: data.access_token,
|
|
494
|
+
expiresAt: Date.now() + (seconds - skew) * 1000,
|
|
495
|
+
};
|
|
496
|
+
return data.access_token;
|
|
497
|
+
}
|
|
498
|
+
async #request(path, options = {}) {
|
|
499
|
+
try {
|
|
500
|
+
return await this.#send(path, options, await this.#accessToken());
|
|
501
|
+
}
|
|
502
|
+
catch (error) {
|
|
503
|
+
// A token can be revoked (or a scope changed) before it expires. One retry with a
|
|
504
|
+
// fresh token; anything else is the caller's problem.
|
|
505
|
+
if (!isUnauthorized(error))
|
|
506
|
+
throw error;
|
|
507
|
+
this.#token = undefined;
|
|
508
|
+
return this.#send(path, options, await this.#accessToken());
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
#send(path, options, token) {
|
|
512
|
+
return httpRequest(path, {
|
|
513
|
+
baseUrl: this.#baseUrl,
|
|
514
|
+
...(options.method !== undefined ? { method: options.method } : {}),
|
|
515
|
+
...(options.body !== undefined ? { body: options.body } : {}),
|
|
516
|
+
...(options.headers !== undefined ? { headers: options.headers } : {}),
|
|
517
|
+
bearerToken: token,
|
|
518
|
+
fetch: this.#fetch,
|
|
519
|
+
});
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
// ── Helpers ────────────────────────────────────────────────────────────────────────────
|
|
523
|
+
/**
|
|
524
|
+
* Efí/BACEN money is a decimal string with two places: 1990 centavos → `'19.90'`.
|
|
525
|
+
* {@link formatDecimal} shifts the digits of the integer instead of dividing, so no
|
|
526
|
+
* binary float ever gets a chance to turn `19.90` into `19.89` on the wire.
|
|
527
|
+
*/
|
|
528
|
+
function toAmountString(amount) {
|
|
529
|
+
return formatDecimal(amount);
|
|
530
|
+
}
|
|
531
|
+
/**
|
|
532
|
+
* The caller's idempotency key as Efí's devolução id, or a boot-loud failure.
|
|
533
|
+
*
|
|
534
|
+
* The devolução id IS the idempotency mechanism on this API — a `PUT` to the same id is
|
|
535
|
+
* the same refund — so the key is used verbatim rather than hashed, which keeps it
|
|
536
|
+
* greppable in Efí's dashboard. BACEN constrains it to 1–35 alphanumerics; a key outside
|
|
537
|
+
* that charset cannot be sent, and quietly minting a random id instead would turn the
|
|
538
|
+
* caller's retry guarantee into a second refund.
|
|
539
|
+
*/
|
|
540
|
+
function idempotencyKeyAsRefundId(key) {
|
|
541
|
+
if (key === undefined)
|
|
542
|
+
return undefined;
|
|
543
|
+
if (!/^[a-zA-Z0-9]{1,35}$/.test(key)) {
|
|
544
|
+
throw new Error(`[payments] Efí deduplicates a refund on the devolução id, and "${key}" cannot be one: BACEN allows 1–35 alphanumeric characters (no dashes, no underscores). Pass an \`idempotencyKey\` in that charset — a UUID works with the dashes stripped.`);
|
|
545
|
+
}
|
|
546
|
+
return key;
|
|
547
|
+
}
|
|
548
|
+
/** The BACEN txid charset: 26–35 alphanumerics. */
|
|
549
|
+
function isValidTxid(value) {
|
|
550
|
+
return /^[a-zA-Z0-9]{26,35}$/.test(value);
|
|
551
|
+
}
|
|
552
|
+
/** An endToEndId looks like `E` + 8-digit ISPB + timestamp + suffix, 32 chars. */
|
|
553
|
+
function isEndToEndId(value) {
|
|
554
|
+
return /^E\d{8}[0-9A-Za-z]{23}$/.test(value);
|
|
555
|
+
}
|
|
556
|
+
function isUnauthorized(error) {
|
|
557
|
+
return (typeof error === 'object' &&
|
|
558
|
+
error !== null &&
|
|
559
|
+
'status' in error &&
|
|
560
|
+
error.status === 401);
|
|
561
|
+
}
|
|
562
|
+
/**
|
|
563
|
+
* A `fetch` that presents Efí's client certificate.
|
|
564
|
+
*
|
|
565
|
+
* This is the whole reason `httpRequest` grew a `fetch` option. Node's global `fetch`
|
|
566
|
+
* gives no way to attach a client certificate — TLS identity belongs to the connection,
|
|
567
|
+
* and the only knob for it is a custom agent/dispatcher. Rather than take on `undici` as a
|
|
568
|
+
* dependency just to build one, this wraps `node:https` (which has accepted `pfx` since
|
|
569
|
+
* forever) in the small part of the `fetch` contract `httpRequest` actually uses: a
|
|
570
|
+
* method, string headers, a string body, and a `Response` back.
|
|
571
|
+
*/
|
|
572
|
+
function mtlsFetch(config) {
|
|
573
|
+
const certificate = config.certificate ?? process.env.EFI_CERTIFICATE;
|
|
574
|
+
if (certificate === undefined) {
|
|
575
|
+
throw new Error("[payments] Efí's Pix API requires mutual TLS and no certificate was configured. " +
|
|
576
|
+
'Generate the .p12 in the Efí dashboard (API → Meus Certificados), then set `EFI_CERTIFICATE` to its path ' +
|
|
577
|
+
'or pass `certificate` (a path or a Buffer) to `payments.efi()`. ' +
|
|
578
|
+
'If you terminate TLS somewhere else, pass your own `fetch` instead.');
|
|
579
|
+
}
|
|
580
|
+
let content;
|
|
581
|
+
if (typeof certificate === 'string') {
|
|
582
|
+
try {
|
|
583
|
+
content = readFileSync(certificate);
|
|
584
|
+
}
|
|
585
|
+
catch (error) {
|
|
586
|
+
throw new Error(`[payments] Efí certificate could not be read from "${certificate}": ${error instanceof Error ? error.message : String(error)}. Pass a readable path, or a Buffer with the certificate's contents.`);
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
else {
|
|
590
|
+
content = Buffer.from(certificate);
|
|
591
|
+
}
|
|
592
|
+
const isPem = content.subarray(0, 64).toString('utf8').includes('-----BEGIN');
|
|
593
|
+
const agent = new HttpsAgent({
|
|
594
|
+
keepAlive: true,
|
|
595
|
+
// Efí hands out a .p12 by default; a converted .pem carries both halves in one file.
|
|
596
|
+
...(isPem
|
|
597
|
+
? { cert: content, key: content }
|
|
598
|
+
: { pfx: content, passphrase: config.certificatePassphrase ?? '' }),
|
|
599
|
+
});
|
|
600
|
+
return async (input, init) => {
|
|
601
|
+
const url = new URL(typeof input === 'string' ? input : String(input));
|
|
602
|
+
const headers = {};
|
|
603
|
+
new Headers(init?.headers).forEach((value, name) => {
|
|
604
|
+
headers[name] = value;
|
|
605
|
+
});
|
|
606
|
+
const body = typeof init?.body === 'string' ? init.body : undefined;
|
|
607
|
+
if (body !== undefined)
|
|
608
|
+
headers['content-length'] = String(Buffer.byteLength(body));
|
|
609
|
+
return new Promise((resolve, reject) => {
|
|
610
|
+
const request = httpsRequest(url, { method: init?.method ?? 'GET', headers, agent }, (response) => {
|
|
611
|
+
const chunks = [];
|
|
612
|
+
response.on('data', (chunk) => chunks.push(chunk));
|
|
613
|
+
response.on('end', () => {
|
|
614
|
+
const status = response.statusCode ?? 502;
|
|
615
|
+
// 204/205/304 must not carry a body or `Response` throws.
|
|
616
|
+
const payload = status === 204 || status === 205 || status === 304 ? null : Buffer.concat(chunks);
|
|
617
|
+
resolve(new Response(payload, { status }));
|
|
618
|
+
});
|
|
619
|
+
response.on('error', reject);
|
|
620
|
+
});
|
|
621
|
+
request.on('error', reject);
|
|
622
|
+
if (body !== undefined)
|
|
623
|
+
request.write(body);
|
|
624
|
+
request.end();
|
|
625
|
+
});
|
|
626
|
+
};
|
|
627
|
+
}
|
|
628
|
+
//# sourceMappingURL=efi.js.map
|