@adonis-agora/payments 0.2.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +2375 -0
- package/dist/assets/spa/assets/index-B7rU8eIL.css +2 -0
- package/dist/assets/spa/assets/index-BcM2VH5k.js +10 -0
- package/dist/assets/spa/assets/index-BcM2VH5k.js.map +1 -0
- package/dist/assets/spa/index.html +20 -0
- package/dist/commands/main.d.ts +4 -3
- package/dist/commands/main.d.ts.map +1 -1
- package/dist/commands/main.js +9 -2
- package/dist/commands/main.js.map +1 -1
- package/dist/commands/make_billable.d.ts.map +1 -1
- package/dist/commands/make_billable.js +1 -1
- package/dist/commands/make_billable.js.map +1 -1
- package/dist/commands/make_webhook_handler.d.ts.map +1 -1
- package/dist/commands/make_webhook_handler.js +1 -1
- package/dist/commands/make_webhook_handler.js.map +1 -1
- package/dist/commands/payments_health.d.ts +31 -0
- package/dist/commands/payments_health.d.ts.map +1 -0
- package/dist/commands/payments_health.js +113 -0
- package/dist/commands/payments_health.js.map +1 -0
- package/dist/commands/payments_sync.d.ts +18 -4
- package/dist/commands/payments_sync.d.ts.map +1 -1
- package/dist/commands/payments_sync.js +136 -40
- package/dist/commands/payments_sync.js.map +1 -1
- package/dist/commands/payments_webhook.d.ts.map +1 -1
- package/dist/commands/payments_webhook.js.map +1 -1
- package/dist/configure.d.ts +12 -5
- package/dist/configure.d.ts.map +1 -1
- package/dist/configure.js +18 -5
- package/dist/configure.js.map +1 -1
- package/dist/providers/dashboard_provider.d.ts +97 -0
- package/dist/providers/dashboard_provider.d.ts.map +1 -0
- package/dist/providers/dashboard_provider.js +430 -0
- package/dist/providers/dashboard_provider.js.map +1 -0
- package/dist/providers/payments_client_provider.d.ts +41 -0
- package/dist/providers/payments_client_provider.d.ts.map +1 -0
- package/dist/providers/payments_client_provider.js +95 -0
- package/dist/providers/payments_client_provider.js.map +1 -0
- package/dist/providers/payments_provider.d.ts +35 -0
- package/dist/providers/payments_provider.d.ts.map +1 -1
- package/dist/providers/payments_provider.js +250 -36
- package/dist/providers/payments_provider.js.map +1 -1
- package/dist/src/billing/billing_health.d.ts +85 -0
- package/dist/src/billing/billing_health.d.ts.map +1 -0
- package/dist/src/billing/billing_health.js +155 -0
- package/dist/src/billing/billing_health.js.map +1 -0
- package/dist/src/billing/billing_overview.d.ts +4 -3
- package/dist/src/billing/billing_overview.d.ts.map +1 -1
- package/dist/src/billing/billing_overview.js +14 -5
- package/dist/src/billing/billing_overview.js.map +1 -1
- package/dist/src/billing/billing_store.d.ts +634 -5
- package/dist/src/billing/billing_store.d.ts.map +1 -1
- package/dist/src/billing/billing_store.js +18 -1
- package/dist/src/billing/billing_store.js.map +1 -1
- package/dist/src/billing/index.d.ts +4 -3
- package/dist/src/billing/index.d.ts.map +1 -1
- package/dist/src/billing/index.js +2 -1
- package/dist/src/billing/index.js.map +1 -1
- package/dist/src/billing/list_query.d.ts +16 -0
- package/dist/src/billing/list_query.d.ts.map +1 -0
- package/dist/src/billing/list_query.js +28 -0
- package/dist/src/billing/list_query.js.map +1 -0
- package/dist/src/billing/lucid_billing_store.d.ts +122 -4
- package/dist/src/billing/lucid_billing_store.d.ts.map +1 -1
- package/dist/src/billing/lucid_billing_store.js +811 -13
- package/dist/src/billing/lucid_billing_store.js.map +1 -1
- package/dist/src/billing/mixins/index.d.ts +5 -2
- package/dist/src/billing/mixins/index.d.ts.map +1 -1
- package/dist/src/billing/mixins/index.js +5 -2
- package/dist/src/billing/mixins/index.js.map +1 -1
- package/dist/src/billing/mixins/with_audit_event.d.ts +48 -0
- package/dist/src/billing/mixins/with_audit_event.d.ts.map +1 -0
- package/dist/src/billing/mixins/with_audit_event.js +77 -0
- package/dist/src/billing/mixins/with_audit_event.js.map +1 -0
- package/dist/src/billing/mixins/with_billable.d.ts.map +1 -1
- package/dist/src/billing/mixins/with_customer.d.ts +39 -0
- package/dist/src/billing/mixins/with_customer.d.ts.map +1 -0
- package/dist/src/billing/mixins/with_customer.js +68 -0
- package/dist/src/billing/mixins/with_customer.js.map +1 -0
- package/dist/src/billing/mixins/with_dispute.d.ts +68 -0
- package/dist/src/billing/mixins/with_dispute.d.ts.map +1 -0
- package/dist/src/billing/mixins/with_dispute.js +83 -0
- package/dist/src/billing/mixins/with_dispute.js.map +1 -0
- package/dist/src/billing/mixins/with_payment.d.ts +42 -0
- package/dist/src/billing/mixins/with_payment.d.ts.map +1 -1
- package/dist/src/billing/mixins/with_payment.js +29 -3
- package/dist/src/billing/mixins/with_payment.js.map +1 -1
- package/dist/src/billing/mixins/with_subscription.d.ts +7 -0
- package/dist/src/billing/mixins/with_subscription.d.ts.map +1 -1
- package/dist/src/billing/mixins/with_subscription.js +15 -1
- package/dist/src/billing/mixins/with_subscription.js.map +1 -1
- package/dist/src/billing/mixins/with_usage_event.d.ts +7 -0
- package/dist/src/billing/mixins/with_usage_event.d.ts.map +1 -1
- package/dist/src/billing/mixins/with_usage_event.js +15 -1
- package/dist/src/billing/mixins/with_usage_event.js.map +1 -1
- package/dist/src/billing/mixins/with_webhook_event.d.ts +18 -0
- package/dist/src/billing/mixins/with_webhook_event.d.ts.map +1 -1
- package/dist/src/billing/mixins/with_webhook_event.js +18 -1
- package/dist/src/billing/mixins/with_webhook_event.js.map +1 -1
- package/dist/src/billing/resolve_dispatch.d.ts +22 -0
- package/dist/src/billing/resolve_dispatch.d.ts.map +1 -0
- package/dist/src/billing/resolve_dispatch.js +44 -0
- package/dist/src/billing/resolve_dispatch.js.map +1 -0
- package/dist/src/billing/resolve_store.d.ts +18 -0
- package/dist/src/billing/resolve_store.d.ts.map +1 -0
- package/dist/src/billing/resolve_store.js +35 -0
- package/dist/src/billing/resolve_store.js.map +1 -0
- package/dist/src/billing/schema.d.ts +108 -0
- package/dist/src/billing/schema.d.ts.map +1 -0
- package/dist/src/billing/schema.js +350 -0
- package/dist/src/billing/schema.js.map +1 -0
- package/dist/src/billing/webhook_dispatcher.d.ts +95 -16
- package/dist/src/billing/webhook_dispatcher.d.ts.map +1 -1
- package/dist/src/billing/webhook_dispatcher.js +186 -33
- package/dist/src/billing/webhook_dispatcher.js.map +1 -1
- package/dist/src/billing/webhook_events.d.ts +67 -1
- package/dist/src/billing/webhook_events.d.ts.map +1 -1
- package/dist/src/billing/webhook_events.js +15 -0
- package/dist/src/billing/webhook_events.js.map +1 -1
- package/dist/src/billing/webhook_processor.d.ts.map +1 -1
- package/dist/src/billing/webhook_processor.js +325 -5
- package/dist/src/billing/webhook_processor.js.map +1 -1
- package/dist/src/client/define_config.d.ts +150 -0
- package/dist/src/client/define_config.d.ts.map +1 -0
- package/dist/src/client/define_config.js +98 -0
- package/dist/src/client/define_config.js.map +1 -0
- package/dist/src/client/handlers.d.ts +71 -0
- package/dist/src/client/handlers.d.ts.map +1 -0
- package/dist/src/client/handlers.js +119 -0
- package/dist/src/client/handlers.js.map +1 -0
- package/dist/src/client/index.d.ts +15 -0
- package/dist/src/client/index.d.ts.map +1 -0
- package/dist/src/client/index.js +12 -0
- package/dist/src/client/index.js.map +1 -0
- package/dist/src/client/statuses.d.ts +38 -0
- package/dist/src/client/statuses.d.ts.map +1 -0
- package/dist/src/client/statuses.js +42 -0
- package/dist/src/client/statuses.js.map +1 -0
- package/dist/src/dashboard/actions.d.ts +143 -0
- package/dist/src/dashboard/actions.d.ts.map +1 -0
- package/dist/src/dashboard/actions.js +112 -0
- package/dist/src/dashboard/actions.js.map +1 -0
- package/dist/src/dashboard/auth.d.ts +136 -0
- package/dist/src/dashboard/auth.d.ts.map +1 -0
- package/dist/src/dashboard/auth.js +152 -0
- package/dist/src/dashboard/auth.js.map +1 -0
- package/dist/src/dashboard/define_config.d.ts +80 -0
- package/dist/src/dashboard/define_config.d.ts.map +1 -0
- package/dist/src/dashboard/define_config.js +83 -0
- package/dist/src/dashboard/define_config.js.map +1 -0
- package/dist/src/dashboard/handlers.d.ts +281 -0
- package/dist/src/dashboard/handlers.d.ts.map +1 -0
- package/dist/src/dashboard/handlers.js +960 -0
- package/dist/src/dashboard/handlers.js.map +1 -0
- package/dist/src/dashboard/index.d.ts +17 -0
- package/dist/src/dashboard/index.d.ts.map +1 -0
- package/dist/src/dashboard/index.js +13 -0
- package/dist/src/dashboard/index.js.map +1 -0
- package/dist/src/dashboard/login_page.d.ts +23 -0
- package/dist/src/dashboard/login_page.d.ts.map +1 -0
- package/dist/src/dashboard/login_page.js +170 -0
- package/dist/src/dashboard/login_page.js.map +1 -0
- package/dist/src/dashboard/period.d.ts +31 -0
- package/dist/src/dashboard/period.d.ts.map +1 -0
- package/dist/src/dashboard/period.js +44 -0
- package/dist/src/dashboard/period.js.map +1 -0
- package/dist/src/dashboard/session_cookie.d.ts +42 -0
- package/dist/src/dashboard/session_cookie.d.ts.map +1 -0
- package/dist/src/dashboard/session_cookie.js +97 -0
- package/dist/src/dashboard/session_cookie.js.map +1 -0
- package/dist/src/dashboard/spa.d.ts +24 -0
- package/dist/src/dashboard/spa.d.ts.map +1 -0
- package/dist/src/dashboard/spa.js +52 -0
- package/dist/src/dashboard/spa.js.map +1 -0
- package/dist/src/define_config.d.ts +245 -10
- package/dist/src/define_config.d.ts.map +1 -1
- package/dist/src/define_config.js +97 -1
- package/dist/src/define_config.js.map +1 -1
- package/dist/src/diagnostics.d.ts +287 -7
- package/dist/src/diagnostics.d.ts.map +1 -1
- package/dist/src/diagnostics.js +317 -1
- package/dist/src/diagnostics.js.map +1 -1
- package/dist/src/driver.d.ts +139 -5
- package/dist/src/driver.d.ts.map +1 -1
- package/dist/src/drivers/abacate.d.ts +13 -3
- package/dist/src/drivers/abacate.d.ts.map +1 -1
- package/dist/src/drivers/abacate.js +112 -19
- package/dist/src/drivers/abacate.js.map +1 -1
- package/dist/src/drivers/adyen.d.ts +164 -0
- package/dist/src/drivers/adyen.d.ts.map +1 -0
- package/dist/src/drivers/adyen.js +678 -0
- package/dist/src/drivers/adyen.js.map +1 -0
- package/dist/src/drivers/asaas.d.ts +53 -3
- package/dist/src/drivers/asaas.d.ts.map +1 -1
- package/dist/src/drivers/asaas.js +364 -29
- package/dist/src/drivers/asaas.js.map +1 -1
- package/dist/src/drivers/dodo.d.ts +135 -0
- package/dist/src/drivers/dodo.d.ts.map +1 -0
- package/dist/src/drivers/dodo.js +864 -0
- package/dist/src/drivers/dodo.js.map +1 -0
- package/dist/src/drivers/efi.d.ts +98 -0
- package/dist/src/drivers/efi.d.ts.map +1 -0
- package/dist/src/drivers/efi.js +628 -0
- package/dist/src/drivers/efi.js.map +1 -0
- package/dist/src/drivers/infinitepay.d.ts +156 -0
- package/dist/src/drivers/infinitepay.d.ts.map +1 -0
- package/dist/src/drivers/infinitepay.js +321 -0
- package/dist/src/drivers/infinitepay.js.map +1 -0
- package/dist/src/drivers/lemonsqueezy.d.ts +125 -0
- package/dist/src/drivers/lemonsqueezy.d.ts.map +1 -0
- package/dist/src/drivers/lemonsqueezy.js +632 -0
- package/dist/src/drivers/lemonsqueezy.js.map +1 -0
- package/dist/src/drivers/mercadopago.d.ts +97 -0
- package/dist/src/drivers/mercadopago.d.ts.map +1 -0
- package/dist/src/drivers/mercadopago.js +844 -0
- package/dist/src/drivers/mercadopago.js.map +1 -0
- package/dist/src/drivers/mollie.d.ts +125 -0
- package/dist/src/drivers/mollie.d.ts.map +1 -0
- package/dist/src/drivers/mollie.js +812 -0
- package/dist/src/drivers/mollie.js.map +1 -0
- package/dist/src/drivers/paddle.d.ts +148 -0
- package/dist/src/drivers/paddle.d.ts.map +1 -0
- package/dist/src/drivers/paddle.js +738 -0
- package/dist/src/drivers/paddle.js.map +1 -0
- package/dist/src/drivers/pagarme.d.ts +97 -0
- package/dist/src/drivers/pagarme.d.ts.map +1 -0
- package/dist/src/drivers/pagarme.js +829 -0
- package/dist/src/drivers/pagarme.js.map +1 -0
- package/dist/src/drivers/pagbank.d.ts +57 -0
- package/dist/src/drivers/pagbank.d.ts.map +1 -0
- package/dist/src/drivers/pagbank.js +499 -0
- package/dist/src/drivers/pagbank.js.map +1 -0
- package/dist/src/drivers/paypal.d.ts +120 -0
- package/dist/src/drivers/paypal.d.ts.map +1 -0
- package/dist/src/drivers/paypal.js +891 -0
- package/dist/src/drivers/paypal.js.map +1 -0
- package/dist/src/drivers/polar.d.ts +118 -0
- package/dist/src/drivers/polar.d.ts.map +1 -0
- package/dist/src/drivers/polar.js +604 -0
- package/dist/src/drivers/polar.js.map +1 -0
- package/dist/src/drivers/razorpay.d.ts +103 -0
- package/dist/src/drivers/razorpay.d.ts.map +1 -0
- package/dist/src/drivers/razorpay.js +867 -0
- package/dist/src/drivers/razorpay.js.map +1 -0
- package/dist/src/drivers/shared.d.ts +33 -0
- package/dist/src/drivers/shared.d.ts.map +1 -0
- package/dist/src/drivers/shared.js +37 -0
- package/dist/src/drivers/shared.js.map +1 -0
- package/dist/src/drivers/square.d.ts +133 -0
- package/dist/src/drivers/square.d.ts.map +1 -0
- package/dist/src/drivers/square.js +905 -0
- package/dist/src/drivers/square.js.map +1 -0
- package/dist/src/drivers/stripe.d.ts +41 -4
- package/dist/src/drivers/stripe.d.ts.map +1 -1
- package/dist/src/drivers/stripe.js +823 -26
- package/dist/src/drivers/stripe.js.map +1 -1
- package/dist/src/drivers/woovi.d.ts +11 -4
- package/dist/src/drivers/woovi.d.ts.map +1 -1
- package/dist/src/drivers/woovi.js +162 -29
- package/dist/src/drivers/woovi.js.map +1 -1
- package/dist/src/ensure_customer.d.ts +34 -1
- package/dist/src/ensure_customer.d.ts.map +1 -1
- package/dist/src/ensure_customer.js +29 -4
- package/dist/src/ensure_customer.js.map +1 -1
- package/dist/src/hooks/webhook_handlers.d.ts.map +1 -1
- package/dist/src/hooks/webhook_handlers.js.map +1 -1
- package/dist/src/http.d.ts +37 -3
- package/dist/src/http.d.ts.map +1 -1
- package/dist/src/http.js +69 -5
- package/dist/src/http.js.map +1 -1
- package/dist/src/index.d.ts +54 -26
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +29 -12
- package/dist/src/index.js.map +1 -1
- package/dist/src/invoice/drivers/asaas.d.ts.map +1 -1
- package/dist/src/invoice/drivers/asaas.js.map +1 -1
- package/dist/src/invoice/drivers/enotas.d.ts.map +1 -1
- package/dist/src/invoice/drivers/enotas.js.map +1 -1
- package/dist/src/invoice/drivers/focus.d.ts.map +1 -1
- package/dist/src/invoice/drivers/focus.js.map +1 -1
- package/dist/src/invoice/drivers/plugnotas.d.ts.map +1 -1
- package/dist/src/invoice/drivers/plugnotas.js.map +1 -1
- package/dist/src/invoice/drivers/tecnospeed.d.ts.map +1 -1
- package/dist/src/invoice/drivers/tecnospeed.js.map +1 -1
- package/dist/src/invoice/emit_invoice.d.ts.map +1 -1
- package/dist/src/invoice/emit_invoice.js +31 -11
- package/dist/src/invoice/emit_invoice.js.map +1 -1
- package/dist/src/invoice/invoice_manager.d.ts.map +1 -1
- package/dist/src/money.d.ts +23 -5
- package/dist/src/money.d.ts.map +1 -1
- package/dist/src/money.js +72 -7
- package/dist/src/money.js.map +1 -1
- package/dist/src/payments_manager.d.ts +8 -1
- package/dist/src/payments_manager.d.ts.map +1 -1
- package/dist/src/payments_manager.js +168 -6
- package/dist/src/payments_manager.js.map +1 -1
- package/dist/src/services/main.d.ts +38 -0
- package/dist/src/services/main.d.ts.map +1 -1
- package/dist/src/services/main.js +130 -0
- package/dist/src/services/main.js.map +1 -1
- package/dist/src/telescope/index.d.ts +2 -0
- package/dist/src/telescope/index.d.ts.map +1 -1
- package/dist/src/telescope/index.js +1 -0
- package/dist/src/telescope/index.js.map +1 -1
- package/dist/src/telescope/payments_watcher.d.ts +10 -4
- package/dist/src/telescope/payments_watcher.d.ts.map +1 -1
- package/dist/src/telescope/payments_watcher.js +66 -14
- package/dist/src/telescope/payments_watcher.js.map +1 -1
- package/dist/src/telescope/register.d.ts +62 -0
- package/dist/src/telescope/register.d.ts.map +1 -0
- package/dist/src/telescope/register.js +48 -0
- package/dist/src/telescope/register.js.map +1 -0
- package/dist/src/testing/fake_payments.d.ts +62 -0
- package/dist/src/testing/fake_payments.d.ts.map +1 -0
- package/dist/src/testing/fake_payments.js +82 -0
- package/dist/src/testing/fake_payments.js.map +1 -0
- package/dist/src/testing/fake_payments_driver.d.ts +1 -1
- package/dist/src/testing/fake_payments_driver.d.ts.map +1 -1
- package/dist/src/testing/fake_payments_driver.js.map +1 -1
- package/dist/src/testing/in_memory_billing_store.d.ts +151 -2
- package/dist/src/testing/in_memory_billing_store.d.ts.map +1 -1
- package/dist/src/testing/in_memory_billing_store.js +517 -4
- package/dist/src/testing/in_memory_billing_store.js.map +1 -1
- package/dist/src/testing/index.d.ts +3 -2
- package/dist/src/testing/index.d.ts.map +1 -1
- package/dist/src/testing/index.js +1 -0
- package/dist/src/testing/index.js.map +1 -1
- package/dist/src/testing/mutable_clock.d.ts.map +1 -1
- package/dist/src/types.d.ts +161 -5
- package/dist/src/types.d.ts.map +1 -1
- package/dist/src/types.js +30 -1
- package/dist/src/types.js.map +1 -1
- package/dist/src/webhook_handlers.d.ts +96 -2
- package/dist/src/webhook_handlers.d.ts.map +1 -1
- package/dist/src/webhook_handlers.js +91 -10
- package/dist/src/webhook_handlers.js.map +1 -1
- package/dist/src/webhook_security.d.ts +94 -3
- package/dist/src/webhook_security.d.ts.map +1 -1
- package/dist/src/webhook_security.js +173 -9
- package/dist/src/webhook_security.js.map +1 -1
- package/dist/stubs/config/payments.stub +13 -0
- package/dist/stubs/config/payments_client.stub +42 -0
- package/dist/stubs/config/payments_dashboard.stub +38 -0
- package/dist/stubs/database/migrations/create_billing_tables.stub +35 -77
- package/dist/stubs/main.js.map +1 -1
- package/dist/stubs/make/webhook_handler/main.stub +29 -11
- package/package.json +67 -32
|
@@ -1,6 +1,90 @@
|
|
|
1
1
|
import Stripe from 'stripe';
|
|
2
2
|
import { publishPaymentDiagnostics, publishRefundDiagnostics, publishSubscriptionDiagnostics, } from '../diagnostics.js';
|
|
3
3
|
import { emitInvoiceIfRequested } from '../invoice/emit_invoice.js';
|
|
4
|
+
import { requireCurrency } from './shared.js';
|
|
5
|
+
/**
|
|
6
|
+
* Stripe's dispute evidence fields, verbatim from the Dispute object (API
|
|
7
|
+
* `2025-08-27.basil`).
|
|
8
|
+
*
|
|
9
|
+
* The set is what lets `metadata` reach the twenty-odd fields {@link DisputeEvidence} has
|
|
10
|
+
* no name for — `product_description`, `cancellation_rebuttal`, `access_activity_log` —
|
|
11
|
+
* while a misspelled or invented key is refused here instead of failing the whole
|
|
12
|
+
* submission at the API. A dispute gets ONE submission; it should not be spent on a typo.
|
|
13
|
+
*/
|
|
14
|
+
/**
|
|
15
|
+
* {@link DisputeDocumentKind} onto Stripe's nine evidence file fields.
|
|
16
|
+
*
|
|
17
|
+
* Stripe files evidence by what it proves, one File upload id per field, which is why the
|
|
18
|
+
* shared type addresses a document by kind rather than handing over a bare list of ids: an
|
|
19
|
+
* id with no kind cannot be submitted anywhere.
|
|
20
|
+
*
|
|
21
|
+
* Three kinds have no field of their own and land on `uncategorized_file`: Stripe has no
|
|
22
|
+
* invoice or terms-of-service slot, and `other` is what the slot is for. Two of them in one
|
|
23
|
+
* submission collide there, and the collision is reported rather than silently resolved —
|
|
24
|
+
* pick which one Stripe should see, or pass the other under its own field via `metadata`.
|
|
25
|
+
*/
|
|
26
|
+
const STRIPE_DOCUMENT_FIELDS = {
|
|
27
|
+
receipt: 'receipt',
|
|
28
|
+
customer_communication: 'customer_communication',
|
|
29
|
+
customer_signature: 'customer_signature',
|
|
30
|
+
shipping: 'shipping_documentation',
|
|
31
|
+
service: 'service_documentation',
|
|
32
|
+
refund_policy: 'refund_policy',
|
|
33
|
+
cancellation_policy: 'cancellation_policy',
|
|
34
|
+
duplicate_charge: 'duplicate_charge_documentation',
|
|
35
|
+
invoice: 'uncategorized_file',
|
|
36
|
+
terms: 'uncategorized_file',
|
|
37
|
+
other: 'uncategorized_file',
|
|
38
|
+
};
|
|
39
|
+
const STRIPE_EVIDENCE_FIELDS = new Set([
|
|
40
|
+
'access_activity_log',
|
|
41
|
+
'billing_address',
|
|
42
|
+
'cancellation_policy',
|
|
43
|
+
'cancellation_policy_disclosure',
|
|
44
|
+
'cancellation_rebuttal',
|
|
45
|
+
'customer_communication',
|
|
46
|
+
'customer_email_address',
|
|
47
|
+
'customer_name',
|
|
48
|
+
'customer_purchase_ip',
|
|
49
|
+
'customer_signature',
|
|
50
|
+
'duplicate_charge_documentation',
|
|
51
|
+
'duplicate_charge_explanation',
|
|
52
|
+
'duplicate_charge_id',
|
|
53
|
+
'product_description',
|
|
54
|
+
'receipt',
|
|
55
|
+
'refund_policy',
|
|
56
|
+
'refund_policy_disclosure',
|
|
57
|
+
'refund_refusal_explanation',
|
|
58
|
+
'service_date',
|
|
59
|
+
'service_documentation',
|
|
60
|
+
'shipping_address',
|
|
61
|
+
'shipping_carrier',
|
|
62
|
+
'shipping_date',
|
|
63
|
+
'shipping_documentation',
|
|
64
|
+
'shipping_tracking_number',
|
|
65
|
+
'uncategorized_file',
|
|
66
|
+
'uncategorized_text',
|
|
67
|
+
]);
|
|
68
|
+
/**
|
|
69
|
+
* The nine evidence fields that take a **File upload id** (`file_…`, created with purpose
|
|
70
|
+
* `dispute_evidence`) rather than text.
|
|
71
|
+
*
|
|
72
|
+
* This is the mismatch behind most of the refusals below: a URL is neither a file id nor
|
|
73
|
+
* something the reviewing bank will follow — Stripe's own guidance is that banks review no
|
|
74
|
+
* external content, links included — so a driver that put `receiptUrl` in `receipt` would
|
|
75
|
+
* submit a defense the issuer never reads and report it as sent.
|
|
76
|
+
*/
|
|
77
|
+
const STRIPE_EVIDENCE_FILE_FIELDS = new Set([
|
|
78
|
+
'cancellation_policy',
|
|
79
|
+
'customer_communication',
|
|
80
|
+
'customer_signature',
|
|
81
|
+
'duplicate_charge_documentation',
|
|
82
|
+
'receipt',
|
|
83
|
+
'refund_policy',
|
|
84
|
+
'service_documentation',
|
|
85
|
+
'shipping_documentation',
|
|
86
|
+
'uncategorized_file',
|
|
87
|
+
]);
|
|
4
88
|
/**
|
|
5
89
|
* Stripe driver. Wraps the Stripe SDK and normalizes its API onto the shared
|
|
6
90
|
* {@link PaymentsDriver} contract. The SDK is imported lazily by the factory in
|
|
@@ -9,7 +93,7 @@ import { emitInvoiceIfRequested } from '../invoice/emit_invoice.js';
|
|
|
9
93
|
export class StripeDriver {
|
|
10
94
|
provider = 'stripe';
|
|
11
95
|
supportedMethods = ['pix', 'credit_card', 'boleto', 'undefined'];
|
|
12
|
-
capabilities = { refunds: true, invoices: true, subscriptions: true };
|
|
96
|
+
capabilities = { disputes: true, refunds: true, invoices: true, subscriptions: true };
|
|
13
97
|
#stripe;
|
|
14
98
|
#currency;
|
|
15
99
|
#webhookSecret;
|
|
@@ -23,7 +107,9 @@ export class StripeDriver {
|
|
|
23
107
|
throw new Error('[payments] Stripe driver requires an API key. Set `STRIPE_KEY` env or pass `apiKey` to `payments.stripe()`.');
|
|
24
108
|
}
|
|
25
109
|
this.#stripe = new Stripe(apiKey);
|
|
26
|
-
this
|
|
110
|
+
// The constructor (not the factory) enforces this, because `./drivers/stripe` is a
|
|
111
|
+
// public entry point too — this is the one boundary every path crosses.
|
|
112
|
+
this.#currency = requireCurrency('stripe', config.currency);
|
|
27
113
|
this.#webhookSecret = config.webhookSecret ?? process.env.STRIPE_WEBHOOK_SECRET;
|
|
28
114
|
}
|
|
29
115
|
// ── Customers ────────────────────────────────────────────────────────────────────────
|
|
@@ -35,7 +121,7 @@ export class StripeDriver {
|
|
|
35
121
|
...(input.metadata !== undefined
|
|
36
122
|
? { metadata: input.metadata }
|
|
37
123
|
: {}),
|
|
38
|
-
});
|
|
124
|
+
}, this.#requestOptions(input.idempotencyKey));
|
|
39
125
|
return this.#mapCustomer(customer);
|
|
40
126
|
}
|
|
41
127
|
async findCustomer(customerId) {
|
|
@@ -64,6 +150,9 @@ export class StripeDriver {
|
|
|
64
150
|
}
|
|
65
151
|
// ── Payments ─────────────────────────────────────────────────────────────────────────
|
|
66
152
|
async charge(input) {
|
|
153
|
+
// Without `payment_method_types` Stripe creates the intent with whatever the account's
|
|
154
|
+
// dashboard defaults are, so a charge routed as Pix could come back a card.
|
|
155
|
+
const methodType = this.#mapMethodToStripe(input.method);
|
|
67
156
|
const params = {
|
|
68
157
|
amount: input.amount,
|
|
69
158
|
currency: input.currency ?? this.#currency,
|
|
@@ -71,18 +160,20 @@ export class StripeDriver {
|
|
|
71
160
|
...(input.description !== undefined ? { description: input.description } : {}),
|
|
72
161
|
...(input.paymentMethodId !== undefined ? { payment_method: input.paymentMethodId } : {}),
|
|
73
162
|
...(input.card !== undefined ? { payment_method: input.card.token } : {}),
|
|
163
|
+
...(methodType !== undefined ? { payment_method_types: [methodType] } : {}),
|
|
74
164
|
...(input.metadata !== undefined
|
|
75
165
|
? { metadata: input.metadata }
|
|
76
166
|
: {}),
|
|
77
|
-
...(input.idempotencyKey !== undefined ? {} : {}),
|
|
78
167
|
};
|
|
79
168
|
if (input.idempotencyKey !== undefined) {
|
|
169
|
+
// Stripe never echoes the request header back on the object, so the metadata copy is
|
|
170
|
+
// what lets `payment.payload` trace a charge to the key that created it.
|
|
80
171
|
params.metadata = { ...(params.metadata ?? {}), idempotency_key: input.idempotencyKey };
|
|
81
172
|
}
|
|
82
173
|
if (input.externalReference !== undefined) {
|
|
83
174
|
params.metadata = { ...(params.metadata ?? {}), external_reference: input.externalReference };
|
|
84
175
|
}
|
|
85
|
-
const intent = await this.#stripe.paymentIntents.create(params);
|
|
176
|
+
const intent = await this.#stripe.paymentIntents.create(params, this.#requestOptions(input.idempotencyKey));
|
|
86
177
|
const payment = this.#mapPayment(intent);
|
|
87
178
|
await emitInvoiceIfRequested(this.#invoiceCtx, input, payment, this);
|
|
88
179
|
publishPaymentDiagnostics(payment);
|
|
@@ -99,11 +190,11 @@ export class StripeDriver {
|
|
|
99
190
|
throw error;
|
|
100
191
|
}
|
|
101
192
|
}
|
|
102
|
-
async refund(paymentGatewayId, amount) {
|
|
193
|
+
async refund(paymentGatewayId, amount, options) {
|
|
103
194
|
const refund = await this.#stripe.refunds.create({
|
|
104
195
|
payment_intent: paymentGatewayId,
|
|
105
196
|
...(amount !== undefined ? { amount } : {}),
|
|
106
|
-
});
|
|
197
|
+
}, this.#requestOptions(options?.idempotencyKey));
|
|
107
198
|
const result = {
|
|
108
199
|
id: refund.id,
|
|
109
200
|
gatewayId: refund.id,
|
|
@@ -149,7 +240,7 @@ export class StripeDriver {
|
|
|
149
240
|
],
|
|
150
241
|
}),
|
|
151
242
|
};
|
|
152
|
-
const session = await this.#stripe.checkout.sessions.create(params);
|
|
243
|
+
const session = await this.#stripe.checkout.sessions.create(params, this.#requestOptions(input.idempotencyKey));
|
|
153
244
|
return {
|
|
154
245
|
id: session.id,
|
|
155
246
|
gatewayId: session.id,
|
|
@@ -195,7 +286,7 @@ export class StripeDriver {
|
|
|
195
286
|
}
|
|
196
287
|
: {}),
|
|
197
288
|
};
|
|
198
|
-
const subscription = await this.#stripe.subscriptions.create(params);
|
|
289
|
+
const subscription = await this.#stripe.subscriptions.create(params, this.#requestOptions(input.idempotencyKey));
|
|
199
290
|
const result = this.#mapSubscription(subscription);
|
|
200
291
|
publishSubscriptionDiagnostics(result, 'subscription.created');
|
|
201
292
|
return result;
|
|
@@ -236,7 +327,7 @@ export class StripeDriver {
|
|
|
236
327
|
? { metadata: input.metadata }
|
|
237
328
|
: {}),
|
|
238
329
|
};
|
|
239
|
-
const subscription = await this.#stripe.subscriptions.update(subscriptionGatewayId, params);
|
|
330
|
+
const subscription = await this.#stripe.subscriptions.update(subscriptionGatewayId, params, this.#requestOptions(input.idempotencyKey));
|
|
240
331
|
return this.#mapSubscription(subscription);
|
|
241
332
|
}
|
|
242
333
|
// ── Invoices ─────────────────────────────────────────────────────────────────────────
|
|
@@ -254,7 +345,7 @@ export class StripeDriver {
|
|
|
254
345
|
provider: this.provider,
|
|
255
346
|
...(typeof invoice.customer === 'string' ? { customerId: invoice.customer } : {}),
|
|
256
347
|
...(subscriptionId !== undefined ? { subscriptionId } : {}),
|
|
257
|
-
status: invoice.status
|
|
348
|
+
status: this.#mapInvoiceStatus(invoice.status),
|
|
258
349
|
amount: { amount: invoice.amount_due, currency: invoice.currency },
|
|
259
350
|
createdAt: new Date(invoice.created * 1000).toISOString(),
|
|
260
351
|
...(invoice.hosted_invoice_url !== null && invoice.hosted_invoice_url !== undefined
|
|
@@ -264,7 +355,71 @@ export class StripeDriver {
|
|
|
264
355
|
};
|
|
265
356
|
});
|
|
266
357
|
}
|
|
358
|
+
// ── Disputes ─────────────────────────────────────────────────────────────────────────
|
|
359
|
+
/**
|
|
360
|
+
* Read a dispute by its Stripe id (`du_…`) — `GET /v1/disputes/{id}`.
|
|
361
|
+
*
|
|
362
|
+
* The two fields an operator acts on come off Stripe's own `evidence_details` rather
|
|
363
|
+
* than off a reading of the status: `evidenceDueBy` from `due_by`, `canSubmitEvidence`
|
|
364
|
+
* from the status **and** `past_due`. `payload` carries the whole Dispute, including
|
|
365
|
+
* `is_charge_refundable` — the other half of the decision, because while it is true a
|
|
366
|
+
* refund can still end the matter, and after a full refund Stripe withdraws nothing
|
|
367
|
+
* further.
|
|
368
|
+
*/
|
|
369
|
+
async findDispute(disputeGatewayId) {
|
|
370
|
+
try {
|
|
371
|
+
return this.#mapDispute(await this.#stripe.disputes.retrieve(disputeGatewayId));
|
|
372
|
+
}
|
|
373
|
+
catch (error) {
|
|
374
|
+
if (this.#isNotFound(error))
|
|
375
|
+
return null;
|
|
376
|
+
throw error;
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
/**
|
|
380
|
+
* Submit evidence for a dispute — `POST /v1/disputes/{id}` with the `evidence` hash.
|
|
381
|
+
*
|
|
382
|
+
* **Submitting is final.** Stripe forwards the response to the issuing bank immediately;
|
|
383
|
+
* you cannot edit it, add to it, or send a second one. So this refuses in three places
|
|
384
|
+
* rather than guessing: it maps every {@link DisputeEvidence} field or names the one it
|
|
385
|
+
* cannot (silently dropping a field the caller believed was submitted is the worst bug
|
|
386
|
+
* this file could have), it re-reads the dispute and refuses when Stripe will not accept
|
|
387
|
+
* evidence, and it refuses an empty hash rather than spending the one submission on
|
|
388
|
+
* nothing.
|
|
389
|
+
*
|
|
390
|
+
* It never decides *whether* to fight — that is margin, customer value and fraud
|
|
391
|
+
* history, none of which a library can see. `submit` defaults to `true` because the
|
|
392
|
+
* method is named submit; `metadata.submit: false` stages a draft instead, which stays
|
|
393
|
+
* visible in the API and the Dashboard until someone submits it.
|
|
394
|
+
*/
|
|
395
|
+
async submitDisputeEvidence(disputeGatewayId, evidence) {
|
|
396
|
+
// Mapped first: a field this driver cannot carry should cost the caller an error, not
|
|
397
|
+
// a round trip, and certainly not a half-built submission.
|
|
398
|
+
const { fields, submit } = this.#mapDisputeEvidence(evidence);
|
|
399
|
+
const current = await this.#stripe.disputes.retrieve(disputeGatewayId);
|
|
400
|
+
if (!this.#canSubmitEvidence(current)) {
|
|
401
|
+
const dueBy = current.evidence_details?.due_by;
|
|
402
|
+
const pastDue = current.evidence_details?.past_due === true
|
|
403
|
+
? ' and the evidence deadline has already passed'
|
|
404
|
+
: '';
|
|
405
|
+
const due = typeof dueBy === 'number' ? ` (due by ${new Date(dueBy * 1000).toISOString()})` : '';
|
|
406
|
+
throw new Error(`[payments] Stripe dispute ${disputeGatewayId} will not accept evidence: its status is \`${current.status}\`${pastDue}${due}. Only \`needs_response\` and \`warning_needs_response\` can still be answered — one under review has had its single submission, and a closed one is decided. Read \`canSubmitEvidence\` from \`findDispute()\` before building the case.`);
|
|
407
|
+
}
|
|
408
|
+
const updated = await this.#stripe.disputes.update(disputeGatewayId, {
|
|
409
|
+
evidence: fields,
|
|
410
|
+
submit,
|
|
411
|
+
});
|
|
412
|
+
return this.#mapDispute(updated);
|
|
413
|
+
}
|
|
267
414
|
// ── Webhooks ─────────────────────────────────────────────────────────────────────────
|
|
415
|
+
/**
|
|
416
|
+
* Whether a delivery to `POST /payments/webhook/:provider` can be authenticated.
|
|
417
|
+
*
|
|
418
|
+
* `parseWebhook` already refuses without the secret — declaring it moves the refusal to boot.
|
|
419
|
+
*/
|
|
420
|
+
get webhookVerification() {
|
|
421
|
+
return this.#webhookSecret !== undefined ? 'configured' : 'unconfigured';
|
|
422
|
+
}
|
|
268
423
|
parseWebhook(rawBody, headers) {
|
|
269
424
|
if (!this.#webhookSecret) {
|
|
270
425
|
throw new Error('[payments] Stripe webhook processing requires `STRIPE_WEBHOOK_SECRET` env var.');
|
|
@@ -297,15 +452,7 @@ export class StripeDriver {
|
|
|
297
452
|
gatewayId: intent.id,
|
|
298
453
|
provider: this.provider,
|
|
299
454
|
amount: { amount: intent.amount, currency: intent.currency },
|
|
300
|
-
status: intent.status
|
|
301
|
-
? 'paid'
|
|
302
|
-
: intent.status === 'requires_payment_method' ||
|
|
303
|
-
intent.status === 'requires_action' ||
|
|
304
|
-
intent.status === 'processing'
|
|
305
|
-
? 'pending'
|
|
306
|
-
: intent.status === 'canceled'
|
|
307
|
-
? 'canceled'
|
|
308
|
-
: 'failed',
|
|
455
|
+
status: this.#mapIntentStatus(intent.status),
|
|
309
456
|
payload: intent.metadata !== undefined && Object.keys(intent.metadata).length > 0
|
|
310
457
|
? intent.metadata
|
|
311
458
|
: {},
|
|
@@ -318,9 +465,70 @@ export class StripeDriver {
|
|
|
318
465
|
if (intent.status === 'succeeded') {
|
|
319
466
|
result.paidAt = new Date(intent.created * 1000).toISOString();
|
|
320
467
|
}
|
|
468
|
+
// A Pix/boleto intent is useless to the caller without what the payer has to act on;
|
|
469
|
+
// Stripe puts it under `next_action`. `image_url_png` is a URL, not the base64 PNG
|
|
470
|
+
// `pixQrCodeImage` promises, so it is deliberately not mapped there.
|
|
471
|
+
const pix = intent.next_action?.pix_display_qr_code;
|
|
472
|
+
if (pix?.data !== undefined) {
|
|
473
|
+
result.pixCode = pix.data;
|
|
474
|
+
result.pixCopiaECola = pix.data;
|
|
475
|
+
}
|
|
476
|
+
const hostedUrl = pix?.hosted_instructions_url ??
|
|
477
|
+
intent.next_action?.boleto_display_details?.hosted_voucher_url;
|
|
478
|
+
if (hostedUrl !== null && hostedUrl !== undefined)
|
|
479
|
+
result.hostedUrl = hostedUrl;
|
|
321
480
|
return result;
|
|
322
481
|
}
|
|
323
|
-
|
|
482
|
+
/**
|
|
483
|
+
* A PaymentIntent status → a `BillingStatus`.
|
|
484
|
+
*
|
|
485
|
+
* `requires_capture` is the manual-capture hold: Stripe has the funds reserved on the
|
|
486
|
+
* card and nothing has moved. It used to fall through to `failed` — the same shape of
|
|
487
|
+
* lie in the opposite direction, a live authorization reported as a dead payment — and
|
|
488
|
+
* `requires_confirmation` fell there with it. Neither is a failure; the first is
|
|
489
|
+
* `authorized` and the second is a payment nobody has confirmed yet.
|
|
490
|
+
*/
|
|
491
|
+
#mapIntentStatus(status) {
|
|
492
|
+
switch (status) {
|
|
493
|
+
case 'succeeded':
|
|
494
|
+
return 'paid';
|
|
495
|
+
case 'requires_capture':
|
|
496
|
+
return 'authorized';
|
|
497
|
+
case 'requires_payment_method':
|
|
498
|
+
case 'requires_confirmation':
|
|
499
|
+
case 'requires_action':
|
|
500
|
+
case 'processing':
|
|
501
|
+
return 'pending';
|
|
502
|
+
case 'canceled':
|
|
503
|
+
return 'canceled';
|
|
504
|
+
default:
|
|
505
|
+
return 'failed';
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
/**
|
|
509
|
+
* A Stripe invoice status → an `Invoice['status']`. stripe-node's enums are open-ended (every
|
|
510
|
+
* union carries a forward-compatible string member), so an unrecognised value falls back to
|
|
511
|
+
* `draft` — the same default a missing status always took — instead of leaking into the contract.
|
|
512
|
+
*/
|
|
513
|
+
#mapInvoiceStatus(status) {
|
|
514
|
+
switch (status) {
|
|
515
|
+
case 'open':
|
|
516
|
+
case 'paid':
|
|
517
|
+
case 'void':
|
|
518
|
+
case 'uncollectible':
|
|
519
|
+
return status;
|
|
520
|
+
default:
|
|
521
|
+
return 'draft';
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
/**
|
|
525
|
+
* A Stripe subscription status → a `SubscriptionStatus`.
|
|
526
|
+
*
|
|
527
|
+
* `paused` used to answer `active`, which entitled a subscriber nobody is billing —
|
|
528
|
+
* Stripe pauses collection precisely so you can stop serving them. It has its own name
|
|
529
|
+
* now: the subscription exists, it will bill again, and it must not grant access today.
|
|
530
|
+
*/
|
|
531
|
+
#mapSubscriptionStatus(status) {
|
|
324
532
|
const statusMap = {
|
|
325
533
|
trialing: 'trialing',
|
|
326
534
|
active: 'active',
|
|
@@ -329,8 +537,11 @@ export class StripeDriver {
|
|
|
329
537
|
canceled: 'canceled',
|
|
330
538
|
unpaid: 'past_due',
|
|
331
539
|
incomplete_expired: 'ended',
|
|
332
|
-
paused: '
|
|
540
|
+
paused: 'paused',
|
|
333
541
|
};
|
|
542
|
+
return (status !== undefined ? statusMap[status] : undefined) ?? 'active';
|
|
543
|
+
}
|
|
544
|
+
#mapSubscription(subscription) {
|
|
334
545
|
return {
|
|
335
546
|
id: subscription.id,
|
|
336
547
|
gatewayId: subscription.id,
|
|
@@ -338,7 +549,7 @@ export class StripeDriver {
|
|
|
338
549
|
customerId: typeof subscription.customer === 'string'
|
|
339
550
|
? subscription.customer
|
|
340
551
|
: subscription.customer.id,
|
|
341
|
-
status:
|
|
552
|
+
status: this.#mapSubscriptionStatus(subscription.status),
|
|
342
553
|
planId: subscription.items.data[0]?.price.id ?? '',
|
|
343
554
|
...(subscription.items.data[0]?.price.unit_amount !== null
|
|
344
555
|
? {
|
|
@@ -361,23 +572,609 @@ export class StripeDriver {
|
|
|
361
572
|
};
|
|
362
573
|
}
|
|
363
574
|
#mapWebhookEvent(event) {
|
|
575
|
+
const object = event.data.object;
|
|
576
|
+
const normalized = this.#normalizeEvent(event.type, object);
|
|
364
577
|
return {
|
|
365
578
|
id: event.id,
|
|
366
579
|
provider: this.provider,
|
|
367
|
-
type:
|
|
368
|
-
|
|
369
|
-
|
|
580
|
+
type: normalized.type,
|
|
581
|
+
// Guarded: `new Date(undefined * 1000).toISOString()` throws a bare RangeError, which
|
|
582
|
+
// surfaces as a crash rather than as this package's own rejection. A real Stripe event
|
|
583
|
+
// always carries `created`, so this only fires on a malformed body — but a malformed
|
|
584
|
+
// body should be REFUSED, not turned into a stack trace.
|
|
585
|
+
//
|
|
586
|
+
// Deliberately untested: this spec's mocked Stripe SDK fills `created`, so a test here
|
|
587
|
+
// passes with and without the guard, which measures nothing. Reaching it needs the real
|
|
588
|
+
// SDK's `constructEvent` over a hand-signed body.
|
|
589
|
+
createdAt: Number.isFinite(event.created)
|
|
590
|
+
? new Date(event.created * 1000).toISOString()
|
|
591
|
+
: new Date().toISOString(),
|
|
592
|
+
data: normalized.data,
|
|
370
593
|
raw: event,
|
|
371
594
|
};
|
|
372
595
|
}
|
|
596
|
+
/**
|
|
597
|
+
* Stripe's own event type → the canonical one, with the payload the processor syncs on.
|
|
598
|
+
*
|
|
599
|
+
* This driver used to pass `event.type` and the raw Stripe object straight through, so
|
|
600
|
+
* `WebhookProcessor` — which switches on the canonical names — recognized nothing Stripe
|
|
601
|
+
* sent and synced nothing: `billing_payments` stayed empty while every webhook was
|
|
602
|
+
* ledgered as processed. A chargeback could not move a row that was never written.
|
|
603
|
+
*
|
|
604
|
+
* A type is only renamed when the canonical payload can actually be built from the
|
|
605
|
+
* object; when it cannot, the event passes through under its Stripe name with the raw
|
|
606
|
+
* object as `data`, which is what the processor does with anything it does not know.
|
|
607
|
+
* That matters because the built-in handlers THROW on a malformed payload, and a throw
|
|
608
|
+
* inside the webhook route is a 500 that Stripe retries forever.
|
|
609
|
+
*/
|
|
610
|
+
#normalizeEvent(type, object) {
|
|
611
|
+
const passthrough = { type, data: object };
|
|
612
|
+
const as = (canonical, data) => data === undefined ? passthrough : { type: canonical, data: data };
|
|
613
|
+
switch (type) {
|
|
614
|
+
case 'payment_intent.succeeded':
|
|
615
|
+
return as('payment.succeeded', this.#intentData(object));
|
|
616
|
+
case 'payment_intent.payment_failed':
|
|
617
|
+
return as('payment.failed', this.#intentData(object));
|
|
618
|
+
// Real state changes with no canonical event of their own. `amount_capturable_updated`
|
|
619
|
+
// is the manual-capture authorization — money held, not moved — and there is
|
|
620
|
+
// deliberately no `payment.authorized` event for it to become.
|
|
621
|
+
case 'payment_intent.canceled':
|
|
622
|
+
case 'payment_intent.processing':
|
|
623
|
+
case 'payment_intent.requires_action':
|
|
624
|
+
case 'payment_intent.amount_capturable_updated':
|
|
625
|
+
return as('payment.updated', this.#intentData(object));
|
|
626
|
+
case 'charge.refunded': {
|
|
627
|
+
const charge = object;
|
|
628
|
+
// Stripe fires this for a PARTIAL refund too, and the canonical `payment.refunded`
|
|
629
|
+
// marks the whole payment refunded. Only `refunded: true` — Stripe's own "nothing
|
|
630
|
+
// left" flag — means that; a partial one is an update.
|
|
631
|
+
return as(charge.refunded === true ? 'payment.refunded' : 'payment.updated', this.#chargeData(object));
|
|
632
|
+
}
|
|
633
|
+
// The dispute family. `charge.dispute.created` fires for BOTH a chargeback and an
|
|
634
|
+
// inquiry, and the two are not the same event at all: on an inquiry the card network
|
|
635
|
+
// is asking a question and **no funds are withdrawn**, so calling it
|
|
636
|
+
// `payment.disputed` took a paid payment away over a question. Stripe distinguishes
|
|
637
|
+
// them only by the status prefix — an inquiry's is `warning_*`.
|
|
638
|
+
case 'charge.dispute.created': {
|
|
639
|
+
const dispute = object;
|
|
640
|
+
return typeof dispute.status === 'string' && dispute.status.startsWith('warning_')
|
|
641
|
+
? as('payment.dispute_warning', this.#disputeWarningData(object))
|
|
642
|
+
: as('payment.disputed', this.#disputeData(object));
|
|
643
|
+
}
|
|
644
|
+
// An early fraud warning is the issuer's TC40/SAFE fraud report, and it arrives
|
|
645
|
+
// before any dispute exists. Stripe's own figure is that ~80% of them become a fraud
|
|
646
|
+
// dispute if you do nothing, and a refund inside the window stops the chargeback
|
|
647
|
+
// being filed at all — which is worth doing even on one you would win, because the
|
|
648
|
+
// chargeback still counts against the ratio that triggers network monitoring.
|
|
649
|
+
case 'radar.early_fraud_warning.created':
|
|
650
|
+
return as('payment.dispute_warning', this.#fraudWarningData(object));
|
|
651
|
+
case 'charge.dispute.closed': {
|
|
652
|
+
const outcome = this.#disputeOutcome(object);
|
|
653
|
+
// Only a status that names an outcome becomes a close. Anything else stays an
|
|
654
|
+
// update rather than inventing a result for money that has not settled.
|
|
655
|
+
return outcome === undefined
|
|
656
|
+
? as('payment.updated', this.#disputeData(object))
|
|
657
|
+
: as('payment.dispute_closed', this.#disputeClosedData(object, outcome));
|
|
658
|
+
}
|
|
659
|
+
// Funds moving during an open dispute, and evidence being submitted. Neither is a
|
|
660
|
+
// resolution.
|
|
661
|
+
case 'charge.dispute.updated':
|
|
662
|
+
case 'charge.dispute.funds_withdrawn':
|
|
663
|
+
case 'charge.dispute.funds_reinstated':
|
|
664
|
+
return as('payment.updated', this.#disputeData(object));
|
|
665
|
+
case 'checkout.session.completed':
|
|
666
|
+
case 'checkout.session.async_payment_succeeded': {
|
|
667
|
+
const session = object;
|
|
668
|
+
// A completed session is not a paid one: a subscription checkout completes with no
|
|
669
|
+
// payment at all, and a delayed-notification method (boleto, SEPA) completes while
|
|
670
|
+
// the money is still in flight. `payment_status` is the only field that says so.
|
|
671
|
+
return as(session.payment_status === 'paid' ? 'payment.succeeded' : 'payment.updated', this.#sessionData(object));
|
|
672
|
+
}
|
|
673
|
+
case 'checkout.session.async_payment_failed':
|
|
674
|
+
return as('payment.failed', this.#sessionData(object));
|
|
675
|
+
case 'checkout.session.expired':
|
|
676
|
+
return as('payment.updated', this.#sessionData(object));
|
|
677
|
+
case 'customer.subscription.created':
|
|
678
|
+
return as('subscription.created', this.#subscriptionData(object));
|
|
679
|
+
case 'customer.subscription.updated':
|
|
680
|
+
case 'customer.subscription.paused':
|
|
681
|
+
case 'customer.subscription.resumed':
|
|
682
|
+
case 'customer.subscription.trial_will_end':
|
|
683
|
+
return as('subscription.updated', this.#subscriptionData(object));
|
|
684
|
+
case 'customer.subscription.deleted':
|
|
685
|
+
return as('subscription.canceled', this.#subscriptionData(object));
|
|
686
|
+
default:
|
|
687
|
+
// Everything else keeps its Stripe name and its raw object: unknown is ledgered
|
|
688
|
+
// and handed to a registered handler, never dropped.
|
|
689
|
+
return passthrough;
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
/** A PaymentIntent → the canonical payment payload, or `undefined` if it is not one. */
|
|
693
|
+
#intentData(object) {
|
|
694
|
+
const intent = object;
|
|
695
|
+
if (typeof intent.id !== 'string' ||
|
|
696
|
+
typeof intent.amount !== 'number' ||
|
|
697
|
+
typeof intent.currency !== 'string') {
|
|
698
|
+
return undefined;
|
|
699
|
+
}
|
|
700
|
+
const metadata = (intent.metadata ?? {});
|
|
701
|
+
return {
|
|
702
|
+
gatewayId: intent.id,
|
|
703
|
+
amount: intent.amount,
|
|
704
|
+
currency: intent.currency,
|
|
705
|
+
...(typeof intent.customer === 'string' ? { customerId: intent.customer } : {}),
|
|
706
|
+
...(typeof metadata.external_reference === 'string'
|
|
707
|
+
? { externalReference: metadata.external_reference }
|
|
708
|
+
: {}),
|
|
709
|
+
};
|
|
710
|
+
}
|
|
711
|
+
/**
|
|
712
|
+
* A Charge → the canonical payment payload, keyed on the **PaymentIntent**.
|
|
713
|
+
*
|
|
714
|
+
* `charge()` returns `pi_…` and every other event here is keyed on it, so a refund
|
|
715
|
+
* ledgered under `ch_…` would write a second row for the same money instead of moving
|
|
716
|
+
* the first. The charge id is only used for a legacy Charges-API charge that has no
|
|
717
|
+
* intent at all.
|
|
718
|
+
*/
|
|
719
|
+
#chargeData(object) {
|
|
720
|
+
const charge = object;
|
|
721
|
+
const gatewayId = typeof charge.payment_intent === 'string'
|
|
722
|
+
? charge.payment_intent
|
|
723
|
+
: typeof charge.id === 'string'
|
|
724
|
+
? charge.id
|
|
725
|
+
: undefined;
|
|
726
|
+
if (gatewayId === undefined ||
|
|
727
|
+
typeof charge.amount !== 'number' ||
|
|
728
|
+
typeof charge.currency !== 'string') {
|
|
729
|
+
return undefined;
|
|
730
|
+
}
|
|
731
|
+
const metadata = (charge.metadata ?? {});
|
|
732
|
+
return {
|
|
733
|
+
gatewayId,
|
|
734
|
+
amount: charge.amount,
|
|
735
|
+
currency: charge.currency,
|
|
736
|
+
...(typeof charge.customer === 'string' ? { customerId: charge.customer } : {}),
|
|
737
|
+
...(typeof metadata.external_reference === 'string'
|
|
738
|
+
? { externalReference: metadata.external_reference }
|
|
739
|
+
: {}),
|
|
740
|
+
};
|
|
741
|
+
}
|
|
742
|
+
/**
|
|
743
|
+
* A Dispute → the canonical payment payload.
|
|
744
|
+
*
|
|
745
|
+
* `payment_intent` is nullable on the Dispute object (a charge created without one has
|
|
746
|
+
* none), so the charge id is the fallback rather than an assumption. The amount is the
|
|
747
|
+
* DISPUTED amount, which for a partial dispute is less than the payment — the row moves
|
|
748
|
+
* to `disputed` either way, and `event.raw` carries the reason and the evidence deadline.
|
|
749
|
+
*/
|
|
750
|
+
#disputeData(object) {
|
|
751
|
+
const dispute = object;
|
|
752
|
+
const gatewayId = typeof dispute.payment_intent === 'string'
|
|
753
|
+
? dispute.payment_intent
|
|
754
|
+
: typeof dispute.charge === 'string'
|
|
755
|
+
? dispute.charge
|
|
756
|
+
: undefined;
|
|
757
|
+
if (gatewayId === undefined ||
|
|
758
|
+
typeof dispute.amount !== 'number' ||
|
|
759
|
+
typeof dispute.currency !== 'string') {
|
|
760
|
+
return undefined;
|
|
761
|
+
}
|
|
762
|
+
return { gatewayId, amount: dispute.amount, currency: dispute.currency };
|
|
763
|
+
}
|
|
764
|
+
/**
|
|
765
|
+
* An inquiry (`warning_*`) or the pre-dispute half of the dispute family. Carries the
|
|
766
|
+
* deadline, which is the entire value of the alert: an inquiry left unanswered reads to
|
|
767
|
+
* the issuer as accepting the claim and becomes a chargeback that is probably
|
|
768
|
+
* irreversible.
|
|
769
|
+
*/
|
|
770
|
+
#disputeWarningData(object) {
|
|
771
|
+
const base = this.#disputeData(object);
|
|
772
|
+
if (base === undefined)
|
|
773
|
+
return undefined;
|
|
774
|
+
const dispute = object;
|
|
775
|
+
const dueBy = dispute.evidence_details?.due_by;
|
|
776
|
+
return {
|
|
777
|
+
...base,
|
|
778
|
+
disputeId: dispute.id,
|
|
779
|
+
...(typeof dispute.reason === 'string' ? { reason: dispute.reason } : {}),
|
|
780
|
+
...(typeof dueBy === 'number'
|
|
781
|
+
? { actionableUntil: new Date(dueBy * 1000).toISOString() }
|
|
782
|
+
: {}),
|
|
783
|
+
};
|
|
784
|
+
}
|
|
785
|
+
/**
|
|
786
|
+
* An early fraud warning has no deadline of its own — the window closes when the
|
|
787
|
+
* chargeback is filed, which is the thing you are trying to prevent — so it carries no
|
|
788
|
+
* `actionableUntil`. `actionable` is Stripe's own flag for whether anything can still be
|
|
789
|
+
* done: false once a dispute has arrived or the charge is fully refunded.
|
|
790
|
+
*/
|
|
791
|
+
#fraudWarningData(object) {
|
|
792
|
+
const warning = object;
|
|
793
|
+
const gatewayId = typeof warning.payment_intent === 'string'
|
|
794
|
+
? warning.payment_intent
|
|
795
|
+
: typeof warning.charge === 'string'
|
|
796
|
+
? warning.charge
|
|
797
|
+
: undefined;
|
|
798
|
+
if (gatewayId === undefined)
|
|
799
|
+
return undefined;
|
|
800
|
+
return {
|
|
801
|
+
gatewayId,
|
|
802
|
+
disputeId: warning.id,
|
|
803
|
+
reason: warning.fraud_type,
|
|
804
|
+
actionable: warning.actionable,
|
|
805
|
+
};
|
|
806
|
+
}
|
|
807
|
+
/**
|
|
808
|
+
* `warning_closed` is an inquiry that sat 120 days without escalating. The networks send
|
|
809
|
+
* no explicit win for one, so it is `expired` rather than `won` — nothing was decided in
|
|
810
|
+
* your favour, the clock simply ran out in the right direction.
|
|
811
|
+
*/
|
|
812
|
+
#disputeOutcome(object) {
|
|
813
|
+
const status = object.status;
|
|
814
|
+
if (status === 'won')
|
|
815
|
+
return 'won';
|
|
816
|
+
if (status === 'lost')
|
|
817
|
+
return 'lost';
|
|
818
|
+
if (status === 'warning_closed')
|
|
819
|
+
return 'expired';
|
|
820
|
+
return undefined;
|
|
821
|
+
}
|
|
822
|
+
#disputeClosedData(object, outcome) {
|
|
823
|
+
const base = this.#disputeData(object);
|
|
824
|
+
if (base === undefined)
|
|
825
|
+
return undefined;
|
|
826
|
+
return { ...base, disputeId: object.id, outcome };
|
|
827
|
+
}
|
|
828
|
+
/**
|
|
829
|
+
* A Stripe Dispute → the canonical {@link Dispute}.
|
|
830
|
+
*
|
|
831
|
+
* `amount` is the DISPUTED amount, which on a partial chargeback is less than the
|
|
832
|
+
* payment, and `reason` stays Stripe's own code (`fraudulent`, `product_not_received`, …)
|
|
833
|
+
* because the vocabulary is the network's, not this library's.
|
|
834
|
+
*/
|
|
835
|
+
#mapDispute(dispute) {
|
|
836
|
+
const dueBy = dispute.evidence_details?.due_by;
|
|
837
|
+
return {
|
|
838
|
+
id: dispute.id,
|
|
839
|
+
provider: this.provider,
|
|
840
|
+
paymentGatewayId: this.#disputedPaymentId(dispute),
|
|
841
|
+
status: this.#mapDisputeStatus(dispute.status),
|
|
842
|
+
...(typeof dispute.amount === 'number' && typeof dispute.currency === 'string'
|
|
843
|
+
? { amount: { amount: dispute.amount, currency: dispute.currency } }
|
|
844
|
+
: {}),
|
|
845
|
+
...(typeof dispute.reason === 'string' ? { reason: dispute.reason } : {}),
|
|
846
|
+
...(typeof dueBy === 'number' ? { evidenceDueBy: new Date(dueBy * 1000).toISOString() } : {}),
|
|
847
|
+
canSubmitEvidence: this.#canSubmitEvidence(dispute),
|
|
848
|
+
...(typeof dispute.created === 'number'
|
|
849
|
+
? { createdAt: new Date(dispute.created * 1000).toISOString() }
|
|
850
|
+
: {}),
|
|
851
|
+
payload: dispute,
|
|
852
|
+
};
|
|
853
|
+
}
|
|
854
|
+
/**
|
|
855
|
+
* The disputed payment's id — the PaymentIntent, falling back to the charge.
|
|
856
|
+
*
|
|
857
|
+
* Same order as the webhook mappers, and for the same reason: `payment_intent` is what
|
|
858
|
+
* `charge()` returns, but it is nullable (a legacy Charges-API charge has none). Both are
|
|
859
|
+
* expandable, so an expanded object is read for its id rather than stringified.
|
|
860
|
+
*/
|
|
861
|
+
#disputedPaymentId(dispute) {
|
|
862
|
+
const { payment_intent: intent, charge } = dispute;
|
|
863
|
+
if (typeof intent === 'string')
|
|
864
|
+
return intent;
|
|
865
|
+
if (intent !== null && typeof intent === 'object')
|
|
866
|
+
return intent.id;
|
|
867
|
+
if (typeof charge === 'string')
|
|
868
|
+
return charge;
|
|
869
|
+
if (charge !== null && typeof charge === 'object')
|
|
870
|
+
return charge.id;
|
|
871
|
+
return '';
|
|
872
|
+
}
|
|
873
|
+
/**
|
|
874
|
+
* A Stripe dispute status → the canonical {@link DisputeStatus}.
|
|
875
|
+
*
|
|
876
|
+
* The `warning_*` statuses are inquiries: the network is asking a question and **no
|
|
877
|
+
* funds have been withdrawn**, so they are `warning` and not `open`/`under_review` —
|
|
878
|
+
* the same money line the webhook mapping draws. `warning_closed` is an inquiry that
|
|
879
|
+
* lapsed without escalating, which is `expired` rather than `won`: nothing was decided
|
|
880
|
+
* in your favour, the clock simply ran out the right way. `prevented` — a dispute
|
|
881
|
+
* stopped before it became a formal chargeback — is `canceled`, because the money never
|
|
882
|
+
* left.
|
|
883
|
+
*
|
|
884
|
+
* The parameter is a plain `string`, not `Stripe.Dispute.Status`: `prevented` is in
|
|
885
|
+
* Stripe's current API reference and NOT in the SDK's union at 18.5.0, so typing this
|
|
886
|
+
* against the SDK would make the branch that handles it unreachable to the compiler
|
|
887
|
+
* while the API keeps sending it.
|
|
888
|
+
*/
|
|
889
|
+
#mapDisputeStatus(status) {
|
|
890
|
+
switch (status) {
|
|
891
|
+
case 'warning_needs_response':
|
|
892
|
+
case 'warning_under_review':
|
|
893
|
+
return 'warning';
|
|
894
|
+
case 'warning_closed':
|
|
895
|
+
return 'expired';
|
|
896
|
+
case 'needs_response':
|
|
897
|
+
return 'open';
|
|
898
|
+
case 'under_review':
|
|
899
|
+
return 'under_review';
|
|
900
|
+
case 'won':
|
|
901
|
+
return 'won';
|
|
902
|
+
case 'lost':
|
|
903
|
+
return 'lost';
|
|
904
|
+
case 'prevented':
|
|
905
|
+
return 'canceled';
|
|
906
|
+
default:
|
|
907
|
+
// A status Stripe adds after this release. `open` is the reading that keeps it in
|
|
908
|
+
// front of an operator, and `payload` still carries Stripe's own word for it.
|
|
909
|
+
return 'open';
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
/**
|
|
913
|
+
* Whether Stripe will still take evidence for this dispute.
|
|
914
|
+
*
|
|
915
|
+
* Read from the status and `evidence_details.past_due`, never inferred from one of them
|
|
916
|
+
* alone: past the deadline the dispute is lost by default and the API rejects the
|
|
917
|
+
* update, and a dispute already `under_review` has spent its one submission. `won`,
|
|
918
|
+
* `lost`, `prevented` and `warning_closed` are over. Some disputes arrive already
|
|
919
|
+
* closed — a network that forbids contesting the reason at all, or a charge Stripe
|
|
920
|
+
* refunded first — and those must read `false` here so the caller learns it before
|
|
921
|
+
* building a case rather than at the API error.
|
|
922
|
+
*/
|
|
923
|
+
#canSubmitEvidence(dispute) {
|
|
924
|
+
if (dispute.evidence_details?.past_due === true)
|
|
925
|
+
return false;
|
|
926
|
+
return dispute.status === 'needs_response' || dispute.status === 'warning_needs_response';
|
|
927
|
+
}
|
|
928
|
+
/**
|
|
929
|
+
* A {@link DisputeEvidence} → Stripe's `evidence` hash, or an error naming what cannot
|
|
930
|
+
* be carried.
|
|
931
|
+
*
|
|
932
|
+
* The rule is that nothing is dropped. Where the shared shape and Stripe's fields
|
|
933
|
+
* disagree — a URL against a File upload id, a count of prior payments against Visa's
|
|
934
|
+
* demand for two specific charges — this throws and says what to send instead, because a
|
|
935
|
+
* caller who believes a receipt was submitted and finds out at the outcome has been told
|
|
936
|
+
* a lie by this library.
|
|
937
|
+
*/
|
|
938
|
+
#mapDisputeEvidence(evidence) {
|
|
939
|
+
if (evidence.termsAcceptedAt !== undefined) {
|
|
940
|
+
throw new Error('[payments] Stripe has no field for WHEN the customer accepted your terms. Its ' +
|
|
941
|
+
'nearest homes are the `refund_policy` / `cancellation_policy` documents with the ' +
|
|
942
|
+
'matching `*_policy_disclosure` text fields, which are where "how and when the ' +
|
|
943
|
+
'customer was shown this" belongs — pass the policy as a `DisputeDocument` and the ' +
|
|
944
|
+
'disclosure through `metadata`.');
|
|
945
|
+
}
|
|
946
|
+
const fields = {};
|
|
947
|
+
/** Every write goes through here, so two sources cannot quietly overwrite each other. */
|
|
948
|
+
const put = (field, value, source) => {
|
|
949
|
+
if (fields[field] !== undefined) {
|
|
950
|
+
throw new Error(`[payments] Stripe dispute evidence field \`${field}\` was set twice — by ${source} and by \`metadata.${field}\`. Pass it once; the driver will not pick a winner.`);
|
|
951
|
+
}
|
|
952
|
+
fields[field] = value;
|
|
953
|
+
};
|
|
954
|
+
if (evidence.explanation !== undefined) {
|
|
955
|
+
put('uncategorized_text', evidence.explanation, '`explanation`');
|
|
956
|
+
}
|
|
957
|
+
if (evidence.shippingCarrier !== undefined) {
|
|
958
|
+
put('shipping_carrier', evidence.shippingCarrier, '`shippingCarrier`');
|
|
959
|
+
}
|
|
960
|
+
if (evidence.shippingTrackingNumber !== undefined) {
|
|
961
|
+
put('shipping_tracking_number', evidence.shippingTrackingNumber, '`shippingTrackingNumber`');
|
|
962
|
+
}
|
|
963
|
+
if (evidence.shippingDate !== undefined) {
|
|
964
|
+
put('shipping_date', evidence.shippingDate, '`shippingDate`');
|
|
965
|
+
}
|
|
966
|
+
if (evidence.serviceDate !== undefined) {
|
|
967
|
+
put('service_date', evidence.serviceDate, '`serviceDate`');
|
|
968
|
+
}
|
|
969
|
+
if (evidence.customerName !== undefined) {
|
|
970
|
+
put('customer_name', evidence.customerName, '`customerName`');
|
|
971
|
+
}
|
|
972
|
+
if (evidence.customerEmail !== undefined) {
|
|
973
|
+
put('customer_email_address', evidence.customerEmail, '`customerEmail`');
|
|
974
|
+
}
|
|
975
|
+
if (evidence.customerIpAddress !== undefined) {
|
|
976
|
+
put('customer_purchase_ip', evidence.customerIpAddress, '`customerIpAddress`');
|
|
977
|
+
}
|
|
978
|
+
for (const document of evidence.documents ?? []) {
|
|
979
|
+
const field = STRIPE_DOCUMENT_FIELDS[document.kind];
|
|
980
|
+
put(field, document.id, `\`documents\` (${document.kind})`);
|
|
981
|
+
}
|
|
982
|
+
if (evidence.priorUndisputedPayments !== undefined) {
|
|
983
|
+
const prior = evidence.priorUndisputedPayments;
|
|
984
|
+
// Stripe's own requirement, not a house rule: Visa Compelling Evidence 3.0 is built
|
|
985
|
+
// around exactly two prior transactions, and Stripe rejects any other count. Failing
|
|
986
|
+
// here costs nothing; failing at the API spends the dispute's single submission.
|
|
987
|
+
if (prior.length !== 2) {
|
|
988
|
+
throw new Error(`[payments] Visa Compelling Evidence 3.0 takes exactly TWO prior undisputed transactions, and got ${prior.length}. Stripe also only accepts them on a dispute it has already marked eligible — check \`enhanced_eligibility_types\` on the dispute payload before building the case.`);
|
|
989
|
+
}
|
|
990
|
+
put('enhanced_evidence', {
|
|
991
|
+
visa_compelling_evidence_3: {
|
|
992
|
+
prior_undisputed_transactions: prior.map((transaction) => ({
|
|
993
|
+
charge: transaction.paymentGatewayId,
|
|
994
|
+
...(transaction.customerAccountId !== undefined
|
|
995
|
+
? { customer_account_id: transaction.customerAccountId }
|
|
996
|
+
: {}),
|
|
997
|
+
...(transaction.customerDeviceId !== undefined
|
|
998
|
+
? { customer_device_id: transaction.customerDeviceId }
|
|
999
|
+
: {}),
|
|
1000
|
+
...(transaction.customerIpAddress !== undefined
|
|
1001
|
+
? { customer_purchase_ip: transaction.customerIpAddress }
|
|
1002
|
+
: {}),
|
|
1003
|
+
})),
|
|
1004
|
+
},
|
|
1005
|
+
}, '`priorUndisputedPayments`');
|
|
1006
|
+
}
|
|
1007
|
+
// `metadata` is the escape hatch the shared type documents, and here it is Stripe's own
|
|
1008
|
+
// field names — the only way to reach `product_description`, the `*_policy` files or
|
|
1009
|
+
// Visa CE 3.0 without inventing fields on a type eighteen drivers share.
|
|
1010
|
+
let submit = true;
|
|
1011
|
+
for (const [key, value] of Object.entries(evidence.metadata ?? {})) {
|
|
1012
|
+
if (key === 'submit') {
|
|
1013
|
+
if (typeof value !== 'boolean') {
|
|
1014
|
+
throw new Error('[payments] `metadata.submit` on Stripe dispute evidence must be a boolean: ' +
|
|
1015
|
+
'`true` (the default) sends the response to the issuer and is final, `false` ' +
|
|
1016
|
+
'stages a draft on the dispute.');
|
|
1017
|
+
}
|
|
1018
|
+
submit = value;
|
|
1019
|
+
continue;
|
|
1020
|
+
}
|
|
1021
|
+
if (key === 'enhanced_evidence') {
|
|
1022
|
+
if (typeof value !== 'object' || value === null) {
|
|
1023
|
+
throw new Error("[payments] `metadata.enhanced_evidence` must be Stripe's enhanced evidence " +
|
|
1024
|
+
'object (`visa_compelling_evidence_3`, `visa_compliance`, ' +
|
|
1025
|
+
'`mastercard_compliance`).');
|
|
1026
|
+
}
|
|
1027
|
+
put('enhanced_evidence', value, '`metadata.enhanced_evidence`');
|
|
1028
|
+
continue;
|
|
1029
|
+
}
|
|
1030
|
+
if (!STRIPE_EVIDENCE_FIELDS.has(key)) {
|
|
1031
|
+
throw new Error(`[payments] \`${key}\` is not a Stripe dispute evidence field, and the driver will not send it — an unrecognized key fails the whole submission at the API. Use Stripe's own field names (https://docs.stripe.com/api/disputes/update), or \`explanation\` for free text.`);
|
|
1032
|
+
}
|
|
1033
|
+
if (typeof value !== 'string') {
|
|
1034
|
+
throw new Error(`[payments] Stripe dispute evidence field \`${key}\` must be a string; got ${typeof value}.`);
|
|
1035
|
+
}
|
|
1036
|
+
if (STRIPE_EVIDENCE_FILE_FIELDS.has(key) && !value.startsWith('file_')) {
|
|
1037
|
+
throw new Error(`[payments] Stripe's \`${key}\` evidence field takes a File upload id (\`file_…\`, created with purpose \`dispute_evidence\`), not \`${value}\`. Upload the document first and pass the id it returns.`);
|
|
1038
|
+
}
|
|
1039
|
+
put(key, value, '`metadata`');
|
|
1040
|
+
}
|
|
1041
|
+
if (Object.keys(fields).length === 0) {
|
|
1042
|
+
throw new Error('[payments] Nothing to submit: the `DisputeEvidence` carried no field Stripe ' +
|
|
1043
|
+
'accepts. Stripe takes ONE submission per dispute and forwards it to the issuer ' +
|
|
1044
|
+
'immediately — an empty one spends it.');
|
|
1045
|
+
}
|
|
1046
|
+
return { fields: fields, submit };
|
|
1047
|
+
}
|
|
1048
|
+
/** A Checkout Session → the canonical payment payload, keyed on its PaymentIntent. */
|
|
1049
|
+
#sessionData(object) {
|
|
1050
|
+
const session = object;
|
|
1051
|
+
const gatewayId = typeof session.payment_intent === 'string' ? session.payment_intent : undefined;
|
|
1052
|
+
if (gatewayId === undefined ||
|
|
1053
|
+
typeof session.amount_total !== 'number' ||
|
|
1054
|
+
typeof session.currency !== 'string') {
|
|
1055
|
+
return undefined;
|
|
1056
|
+
}
|
|
1057
|
+
const metadata = (session.metadata ?? {});
|
|
1058
|
+
return {
|
|
1059
|
+
gatewayId,
|
|
1060
|
+
amount: session.amount_total,
|
|
1061
|
+
currency: session.currency,
|
|
1062
|
+
...(typeof session.customer === 'string' ? { customerId: session.customer } : {}),
|
|
1063
|
+
...(typeof session.subscription === 'string' ? { subscriptionId: session.subscription } : {}),
|
|
1064
|
+
...(typeof metadata.external_reference === 'string'
|
|
1065
|
+
? { externalReference: metadata.external_reference }
|
|
1066
|
+
: typeof session.client_reference_id === 'string'
|
|
1067
|
+
? { externalReference: session.client_reference_id }
|
|
1068
|
+
: {}),
|
|
1069
|
+
};
|
|
1070
|
+
}
|
|
1071
|
+
/** A Subscription → the canonical subscription payload. */
|
|
1072
|
+
#subscriptionData(object) {
|
|
1073
|
+
const subscription = object;
|
|
1074
|
+
const customerId = typeof subscription.customer === 'string'
|
|
1075
|
+
? subscription.customer
|
|
1076
|
+
: subscription.customer?.id;
|
|
1077
|
+
if (typeof subscription.id !== 'string' || typeof customerId !== 'string')
|
|
1078
|
+
return undefined;
|
|
1079
|
+
const planId = subscription.items?.data?.[0]?.price?.id;
|
|
1080
|
+
return {
|
|
1081
|
+
gatewayId: subscription.id,
|
|
1082
|
+
customerId,
|
|
1083
|
+
status: this.#mapSubscriptionStatus(subscription.status),
|
|
1084
|
+
...(typeof planId === 'string' ? { planId } : {}),
|
|
1085
|
+
...(typeof subscription.trial_end === 'number'
|
|
1086
|
+
? { trialEndsAt: new Date(subscription.trial_end * 1000).toISOString() }
|
|
1087
|
+
: {}),
|
|
1088
|
+
...(typeof subscription.ended_at === 'number'
|
|
1089
|
+
? { endsAt: new Date(subscription.ended_at * 1000).toISOString() }
|
|
1090
|
+
: {}),
|
|
1091
|
+
};
|
|
1092
|
+
}
|
|
1093
|
+
/**
|
|
1094
|
+
* Canonical method → the Stripe `payment_method_types` entry that produces it. An
|
|
1095
|
+
* unnamed (or unmappable) method leaves the intent on the account's dynamic payment
|
|
1096
|
+
* methods, which is Stripe's own default rather than a silent substitution.
|
|
1097
|
+
*/
|
|
1098
|
+
#mapMethodToStripe(method) {
|
|
1099
|
+
switch (method) {
|
|
1100
|
+
case 'pix':
|
|
1101
|
+
return 'pix';
|
|
1102
|
+
case 'boleto':
|
|
1103
|
+
return 'boleto';
|
|
1104
|
+
case 'credit_card':
|
|
1105
|
+
return 'card';
|
|
1106
|
+
default:
|
|
1107
|
+
return undefined;
|
|
1108
|
+
}
|
|
1109
|
+
}
|
|
1110
|
+
/**
|
|
1111
|
+
* Stripe deduplicates on the `Idempotency-Key` request header, which the SDK takes as a
|
|
1112
|
+
* request option — a key sent in the body retries into a second charge.
|
|
1113
|
+
*/
|
|
1114
|
+
#requestOptions(idempotencyKey) {
|
|
1115
|
+
return idempotencyKey !== undefined ? { idempotencyKey } : undefined;
|
|
1116
|
+
}
|
|
1117
|
+
/**
|
|
1118
|
+
* A Stripe `payment_method_types` entry → the canonical {@link PaymentMethodType}.
|
|
1119
|
+
*
|
|
1120
|
+
* By category, never by brand: Stripe adds a local method most quarters, and a driver
|
|
1121
|
+
* that enumerated iDEAL, BLIK, TWINT and the rest would go stale between releases while
|
|
1122
|
+
* `PaymentMethodType` — a closed union — could never keep up. So SEPA and ACH are one
|
|
1123
|
+
* answer (`bank_debit`, pulled from an account), iDEAL and Bancontact another
|
|
1124
|
+
* (`bank_transfer`, pushed from a bank), and the brand stays readable on
|
|
1125
|
+
* `payment.payload`.
|
|
1126
|
+
*
|
|
1127
|
+
* The type is read off `payment_method_types[0]`, which is the list of methods the
|
|
1128
|
+
* intent ALLOWS. With one entry — what this driver sends whenever the charge names a
|
|
1129
|
+
* `method` — it is exact; with the account's dynamic payment methods it is Stripe's own
|
|
1130
|
+
* ordering, and the settled method is only known once `latest_charge` exists.
|
|
1131
|
+
*/
|
|
373
1132
|
#mapMethod(method) {
|
|
374
1133
|
switch (method) {
|
|
375
1134
|
case 'card':
|
|
1135
|
+
case 'card_present':
|
|
376
1136
|
return 'card';
|
|
377
1137
|
case 'pix':
|
|
378
1138
|
return 'pix';
|
|
379
1139
|
case 'boleto':
|
|
380
1140
|
return 'boleto';
|
|
1141
|
+
// Pulled from an account you hold a mandate on.
|
|
1142
|
+
case 'sepa_debit':
|
|
1143
|
+
case 'us_bank_account':
|
|
1144
|
+
case 'acss_debit':
|
|
1145
|
+
case 'bacs_debit':
|
|
1146
|
+
case 'au_becs_debit':
|
|
1147
|
+
return 'bank_debit';
|
|
1148
|
+
// Pushed from the payer's own bank, in their own banking app.
|
|
1149
|
+
case 'ideal':
|
|
1150
|
+
case 'bancontact':
|
|
1151
|
+
case 'eps':
|
|
1152
|
+
case 'p24':
|
|
1153
|
+
case 'blik':
|
|
1154
|
+
case 'multibanco':
|
|
1155
|
+
case 'sofort':
|
|
1156
|
+
case 'customer_balance':
|
|
1157
|
+
return 'bank_transfer';
|
|
1158
|
+
// Stored-balance and device wallets.
|
|
1159
|
+
case 'link':
|
|
1160
|
+
case 'paypal':
|
|
1161
|
+
case 'wechat_pay':
|
|
1162
|
+
case 'alipay':
|
|
1163
|
+
case 'cashapp':
|
|
1164
|
+
case 'revolut_pay':
|
|
1165
|
+
case 'amazon_pay':
|
|
1166
|
+
case 'twint':
|
|
1167
|
+
return 'wallet';
|
|
1168
|
+
case 'klarna':
|
|
1169
|
+
case 'afterpay_clearpay':
|
|
1170
|
+
case 'affirm':
|
|
1171
|
+
case 'zip':
|
|
1172
|
+
return 'bnpl';
|
|
1173
|
+
// Paid in cash at a counter against a printed reference.
|
|
1174
|
+
case 'oxxo':
|
|
1175
|
+
case 'konbini':
|
|
1176
|
+
case 'paysafecard':
|
|
1177
|
+
return 'voucher';
|
|
381
1178
|
default:
|
|
382
1179
|
return 'unknown';
|
|
383
1180
|
}
|