@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,738 @@
|
|
|
1
|
+
import { createHmac } from 'node:crypto';
|
|
2
|
+
import { publishRefundDiagnostics, publishSubscriptionDiagnostics } from '../diagnostics.js';
|
|
3
|
+
import { headerValue, httpRequest, isNotFound } from '../http.js';
|
|
4
|
+
import { safeCompare } from '../webhook_security.js';
|
|
5
|
+
import { requireCredential, requireCurrency } from './shared.js';
|
|
6
|
+
/**
|
|
7
|
+
* Paddle driver — **Paddle Billing** (the v2 API on `api.paddle.com`), not the deprecated
|
|
8
|
+
* Paddle Classic vendors API. Paddle is a merchant of record: it is the seller on the
|
|
9
|
+
* customer's statement, calculates and remits sales tax/VAT worldwide, and pays you out.
|
|
10
|
+
* You are selling to Paddle; Paddle is selling to your customer.
|
|
11
|
+
*
|
|
12
|
+
* The consequence for this driver is that Paddle never moves money on a server-side API
|
|
13
|
+
* call. There is no create-and-capture endpoint: every payment is collected by Paddle
|
|
14
|
+
* Checkout (or by a Paddle-issued invoice), so `charge()` throws and `createCheckout()` is
|
|
15
|
+
* the entry point. Subscriptions likewise cannot be created over the API — Paddle creates
|
|
16
|
+
* one when a customer completes a checkout for a recurring price.
|
|
17
|
+
*
|
|
18
|
+
* REST via `fetch`; no SDK peer dependency. Paddle signs webhooks with HMAC-SHA256 over
|
|
19
|
+
* `<ts>:<raw body>`, which is a few lines of `node:crypto`.
|
|
20
|
+
*/
|
|
21
|
+
export class PaddleDriver {
|
|
22
|
+
provider = 'paddle';
|
|
23
|
+
/**
|
|
24
|
+
* Paddle Checkout decides which methods to offer from the buyer's country and your
|
|
25
|
+
* account settings; the API takes no payment-method argument on a transaction. So the
|
|
26
|
+
* only honest canonical name is `'undefined'` — "the customer chooses at checkout".
|
|
27
|
+
* Routing `credit_card` here is refused by the manager, which is correct: this driver
|
|
28
|
+
* cannot promise the charge will be a card.
|
|
29
|
+
*/
|
|
30
|
+
supportedMethods = ['undefined'];
|
|
31
|
+
/**
|
|
32
|
+
* `disputes: false`, and on Paddle that is not a gap — it is the product. "The Paddle
|
|
33
|
+
* team contests chargebacks for you", and Paddle's own risk-prevention page says the
|
|
34
|
+
* defense "is fully automated, and additional evidence submitted by sellers is not
|
|
35
|
+
* required or accepted". There is nothing for `submitDisputeEvidence` to submit and no
|
|
36
|
+
* dispute resource to read: Paddle exposes chargebacks only as adjustments.
|
|
37
|
+
*/
|
|
38
|
+
capabilities = { refunds: true, invoices: true, subscriptions: true, disputes: false };
|
|
39
|
+
#baseUrl;
|
|
40
|
+
#apiKey;
|
|
41
|
+
#currency;
|
|
42
|
+
#productId;
|
|
43
|
+
#webhookSecret;
|
|
44
|
+
#webhookMaxAgeSeconds;
|
|
45
|
+
constructor(_ctx, config) {
|
|
46
|
+
this.#apiKey = requireCredential({
|
|
47
|
+
driver: 'paddle',
|
|
48
|
+
option: 'apiKey',
|
|
49
|
+
env: 'PADDLE_API_KEY',
|
|
50
|
+
value: config.apiKey,
|
|
51
|
+
});
|
|
52
|
+
// Paddle bills in whatever currency the transaction names, so there is no safe
|
|
53
|
+
// default — the same reasoning as Stripe.
|
|
54
|
+
this.#currency = requireCurrency('paddle', config.currency);
|
|
55
|
+
const sandbox = config.sandbox ?? process.env.NODE_ENV !== 'production';
|
|
56
|
+
this.#baseUrl = sandbox ? 'https://sandbox-api.paddle.com' : 'https://api.paddle.com';
|
|
57
|
+
this.#productId = config.productId ?? process.env.PADDLE_PRODUCT_ID;
|
|
58
|
+
// An empty env var is "unset", not "a secret that can never match" — otherwise a
|
|
59
|
+
// blank `PADDLE_WEBHOOK_SECRET` fails every webhook with a signature error instead of
|
|
60
|
+
// the message telling you to configure it.
|
|
61
|
+
const webhookSecret = config.webhookSecret ?? process.env.PADDLE_WEBHOOK_SECRET;
|
|
62
|
+
this.#webhookSecret = webhookSecret === '' ? undefined : webhookSecret;
|
|
63
|
+
this.#webhookMaxAgeSeconds = config.webhookMaxAgeSeconds;
|
|
64
|
+
}
|
|
65
|
+
// ── Customers ────────────────────────────────────────────────────────────────────────
|
|
66
|
+
async createCustomer(input) {
|
|
67
|
+
this.#refuseIdempotencyKey(input.idempotencyKey, 'createCustomer');
|
|
68
|
+
if (input.email === undefined) {
|
|
69
|
+
throw new Error('[payments] Paddle requires an email to create a customer.');
|
|
70
|
+
}
|
|
71
|
+
this.#refuseTaxId(input.taxId);
|
|
72
|
+
const body = {
|
|
73
|
+
email: input.email,
|
|
74
|
+
...(input.name !== undefined ? { name: input.name } : {}),
|
|
75
|
+
...(input.metadata !== undefined ? { custom_data: input.metadata } : {}),
|
|
76
|
+
};
|
|
77
|
+
const { data } = await this.#request('/customers', {
|
|
78
|
+
method: 'POST',
|
|
79
|
+
body,
|
|
80
|
+
});
|
|
81
|
+
return this.#mapCustomer(data);
|
|
82
|
+
}
|
|
83
|
+
async findCustomer(customerId) {
|
|
84
|
+
try {
|
|
85
|
+
const { data } = await this.#request(`/customers/${encodeURIComponent(customerId)}`);
|
|
86
|
+
return this.#mapCustomer(data);
|
|
87
|
+
}
|
|
88
|
+
catch (error) {
|
|
89
|
+
if (isNotFound(error))
|
|
90
|
+
return null;
|
|
91
|
+
throw error;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
async updateCustomer(customerId, input) {
|
|
95
|
+
this.#refuseTaxId(input.taxId);
|
|
96
|
+
const body = {
|
|
97
|
+
...(input.email !== undefined ? { email: input.email } : {}),
|
|
98
|
+
...(input.name !== undefined ? { name: input.name } : {}),
|
|
99
|
+
...(input.metadata !== undefined ? { custom_data: input.metadata } : {}),
|
|
100
|
+
};
|
|
101
|
+
const { data } = await this.#request(`/customers/${encodeURIComponent(customerId)}`, { method: 'PATCH', body });
|
|
102
|
+
return this.#mapCustomer(data);
|
|
103
|
+
}
|
|
104
|
+
// ── Payments ─────────────────────────────────────────────────────────────────────────
|
|
105
|
+
/**
|
|
106
|
+
* Paddle has no endpoint that takes money. `POST /transactions` creates an unpaid
|
|
107
|
+
* record whose only route to payment is the checkout URL it returns — which is exactly
|
|
108
|
+
* what {@link createCheckout} does. Returning a `Payment` here would advertise a capture
|
|
109
|
+
* that never happened.
|
|
110
|
+
*/
|
|
111
|
+
async charge(_input) {
|
|
112
|
+
throw new Error('[payments] Paddle cannot charge server-side — as merchant of record it collects ' +
|
|
113
|
+
'every payment through Paddle Checkout. Use `createCheckout()` and act on the ' +
|
|
114
|
+
'`transaction.completed` webhook.');
|
|
115
|
+
}
|
|
116
|
+
async findPayment(gatewayId) {
|
|
117
|
+
try {
|
|
118
|
+
const { data } = await this.#request(`/transactions/${encodeURIComponent(gatewayId)}`);
|
|
119
|
+
return this.#mapPayment(data);
|
|
120
|
+
}
|
|
121
|
+
catch (error) {
|
|
122
|
+
if (isNotFound(error))
|
|
123
|
+
return null;
|
|
124
|
+
throw error;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Refunds are adjustments (`POST /adjustments` with `action: 'refund'`). A full refund
|
|
129
|
+
* needs no line items; a partial one is expressed **per line item**, which `amount`
|
|
130
|
+
* alone cannot address — so a partial refund is only attempted when the transaction has
|
|
131
|
+
* exactly one line item, and refuses otherwise rather than refunding the wrong line.
|
|
132
|
+
*
|
|
133
|
+
* On a live account most refunds come back `pending_approval` until Paddle reviews them,
|
|
134
|
+
* so a `'pending'` status here is normal and not a failure.
|
|
135
|
+
*
|
|
136
|
+
* `options.idempotencyKey` is REFUSED, not ignored: Paddle deduplicates nothing, so
|
|
137
|
+
* accepting the key would turn a caller's retry guarantee into a second refund.
|
|
138
|
+
*/
|
|
139
|
+
async refund(paymentGatewayId, amount, options) {
|
|
140
|
+
this.#refuseIdempotencyKey(options?.idempotencyKey, 'refund');
|
|
141
|
+
const body = {
|
|
142
|
+
action: 'refund',
|
|
143
|
+
transaction_id: paymentGatewayId,
|
|
144
|
+
reason: 'Requested via @adonis-agora/payments',
|
|
145
|
+
};
|
|
146
|
+
if (amount === undefined) {
|
|
147
|
+
body.type = 'full';
|
|
148
|
+
}
|
|
149
|
+
else {
|
|
150
|
+
const { data: transaction } = await this.#request(`/transactions/${encodeURIComponent(paymentGatewayId)}`);
|
|
151
|
+
const lineItems = transaction.details?.line_items ?? [];
|
|
152
|
+
const itemId = lineItems.length === 1 ? lineItems[0]?.id : undefined;
|
|
153
|
+
if (itemId === undefined) {
|
|
154
|
+
throw new Error(`[payments] Paddle refunds a partial amount per transaction line item, and transaction "${paymentGatewayId}" has ${lineItems.length} of them. Refund the full transaction (omit \`amount\`), or create the adjustment yourself with the item ids you want to refund.`);
|
|
155
|
+
}
|
|
156
|
+
body.type = 'partial';
|
|
157
|
+
// Same string-of-smallest-unit rule as the unit price above — not a decimal.
|
|
158
|
+
body.items = [{ item_id: itemId, type: 'partial', amount: String(amount) }];
|
|
159
|
+
}
|
|
160
|
+
const { data } = await this.#request('/adjustments', {
|
|
161
|
+
method: 'POST',
|
|
162
|
+
body,
|
|
163
|
+
});
|
|
164
|
+
const refund = {
|
|
165
|
+
id: data.id,
|
|
166
|
+
gatewayId: data.id,
|
|
167
|
+
provider: this.provider,
|
|
168
|
+
amount: {
|
|
169
|
+
amount: this.#toCents(data.totals?.total) ?? amount ?? 0,
|
|
170
|
+
currency: (data.totals?.currency_code ?? this.#currency).toLowerCase(),
|
|
171
|
+
},
|
|
172
|
+
status: data.status === 'approved'
|
|
173
|
+
? 'succeeded'
|
|
174
|
+
: data.status === 'pending_approval'
|
|
175
|
+
? 'pending'
|
|
176
|
+
: 'failed',
|
|
177
|
+
createdAt: data.created_at ?? new Date().toISOString(),
|
|
178
|
+
};
|
|
179
|
+
publishRefundDiagnostics(refund);
|
|
180
|
+
return refund;
|
|
181
|
+
}
|
|
182
|
+
// ── Checkout ─────────────────────────────────────────────────────────────────────────
|
|
183
|
+
/**
|
|
184
|
+
* Creates a transaction and hands back the Paddle Checkout URL that pays it. Pass
|
|
185
|
+
* `planId` to sell a catalog price (the only way to start a subscription); without one
|
|
186
|
+
* the driver builds a non-catalog price against `productId`, so nothing is written to
|
|
187
|
+
* your catalog per checkout.
|
|
188
|
+
*/
|
|
189
|
+
async createCheckout(input) {
|
|
190
|
+
this.#refuseIdempotencyKey(input.idempotencyKey, 'createCheckout');
|
|
191
|
+
if (input.trialDays !== undefined) {
|
|
192
|
+
// A Paddle trial lives on the price (`trial_period`), not on the transaction.
|
|
193
|
+
// Accepting the option here would bill a customer who was promised a trial.
|
|
194
|
+
throw new Error('[payments] Paddle configures trials on the price (`trial_period`), not per ' +
|
|
195
|
+
'checkout. Create a price with a trial period and pass its id as `planId`.');
|
|
196
|
+
}
|
|
197
|
+
const currency = (input.currency ?? this.#currency).toUpperCase();
|
|
198
|
+
const items = input.planId !== undefined ? this.#catalogItem(input) : this.#adHocItem(input, currency);
|
|
199
|
+
const customData = this.#customData(input);
|
|
200
|
+
const body = {
|
|
201
|
+
items,
|
|
202
|
+
currency_code: currency,
|
|
203
|
+
collection_mode: 'automatic',
|
|
204
|
+
...(input.customerId !== undefined ? { customer_id: input.customerId } : {}),
|
|
205
|
+
...(customData !== undefined ? { custom_data: customData } : {}),
|
|
206
|
+
// Paddle's `checkout.url` is where the checkout *opens* (your page hosting
|
|
207
|
+
// Paddle.js), not a post-payment redirect: Paddle returns it with `?_ptxn=<id>`
|
|
208
|
+
// appended. `successUrl` is the only URL slot the shared input has, so it is used
|
|
209
|
+
// for that unless `metadata.checkoutUrl` names the hosting page explicitly.
|
|
210
|
+
checkout: { url: input.metadata?.checkoutUrl ?? input.successUrl },
|
|
211
|
+
};
|
|
212
|
+
const { data } = await this.#request('/transactions', { method: 'POST', body });
|
|
213
|
+
const total = this.#toCents(data.details?.totals?.total);
|
|
214
|
+
return {
|
|
215
|
+
id: data.id,
|
|
216
|
+
gatewayId: data.id,
|
|
217
|
+
provider: this.provider,
|
|
218
|
+
url: data.checkout?.url ?? '',
|
|
219
|
+
status: this.#mapCheckoutStatus(data.status),
|
|
220
|
+
...(total !== undefined
|
|
221
|
+
? { amount: { amount: total, currency: data.currency_code.toLowerCase() } }
|
|
222
|
+
: {}),
|
|
223
|
+
...(data.subscription_id ? { subscriptionId: data.subscription_id } : {}),
|
|
224
|
+
...(data.customer_id ? { customerId: data.customer_id } : {}),
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
// ── Subscriptions ────────────────────────────────────────────────────────────────────
|
|
228
|
+
/**
|
|
229
|
+
* Paddle's API has no create-subscription endpoint: a subscription exists only once a
|
|
230
|
+
* customer has completed a checkout (or a manually-collected invoice) for a recurring
|
|
231
|
+
* price. Anything returned here would be a subscription Paddle has never heard of.
|
|
232
|
+
*/
|
|
233
|
+
async createSubscription(_input) {
|
|
234
|
+
throw new Error('[payments] Paddle cannot create a subscription over the API — it creates one when ' +
|
|
235
|
+
'a customer completes a checkout for a recurring price. Call ' +
|
|
236
|
+
"`createCheckout({ planId: '<paddle price id>' })` and read the id off the " +
|
|
237
|
+
'`subscription.created` webhook.');
|
|
238
|
+
}
|
|
239
|
+
async cancelSubscription(subscriptionGatewayId, options) {
|
|
240
|
+
const { data } = await this.#request(`/subscriptions/${encodeURIComponent(subscriptionGatewayId)}/cancel`, {
|
|
241
|
+
method: 'POST',
|
|
242
|
+
body: {
|
|
243
|
+
effective_from: options?.atPeriodEnd === false ? 'immediately' : 'next_billing_period',
|
|
244
|
+
},
|
|
245
|
+
});
|
|
246
|
+
const subscription = this.#mapSubscription(data);
|
|
247
|
+
publishSubscriptionDiagnostics(subscription, 'subscription.canceled');
|
|
248
|
+
return subscription;
|
|
249
|
+
}
|
|
250
|
+
/**
|
|
251
|
+
* Only `metadata` is writable. Paddle has no amount on a subscription — the price is a
|
|
252
|
+
* catalog object and you change what a customer pays by swapping `items[].price_id`,
|
|
253
|
+
* which the shared input has no room for — and no description field at all. Both are
|
|
254
|
+
* refused instead of being dropped on the floor.
|
|
255
|
+
*/
|
|
256
|
+
async updateSubscription(subscriptionGatewayId, input) {
|
|
257
|
+
this.#refuseIdempotencyKey(input.idempotencyKey, 'updateSubscription');
|
|
258
|
+
if (input.amount !== undefined) {
|
|
259
|
+
throw new Error('[payments] Paddle has no editable amount on a subscription — you change what a ' +
|
|
260
|
+
'customer pays by swapping the price on `items[].price_id`. Update the ' +
|
|
261
|
+
'subscription items directly (`PATCH /subscriptions/{id}`) with a ' +
|
|
262
|
+
'`proration_billing_mode`.');
|
|
263
|
+
}
|
|
264
|
+
if (input.description !== undefined) {
|
|
265
|
+
throw new Error('[payments] Paddle subscriptions have no description field.');
|
|
266
|
+
}
|
|
267
|
+
const { data } = await this.#request(`/subscriptions/${encodeURIComponent(subscriptionGatewayId)}`, { method: 'PATCH', body: { custom_data: input.metadata ?? {} } });
|
|
268
|
+
return this.#mapSubscription(data);
|
|
269
|
+
}
|
|
270
|
+
async findSubscription(gatewayId) {
|
|
271
|
+
try {
|
|
272
|
+
const { data } = await this.#request(`/subscriptions/${encodeURIComponent(gatewayId)}`);
|
|
273
|
+
return this.#mapSubscription(data);
|
|
274
|
+
}
|
|
275
|
+
catch (error) {
|
|
276
|
+
if (isNotFound(error))
|
|
277
|
+
return null;
|
|
278
|
+
throw error;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
// ── Invoices ─────────────────────────────────────────────────────────────────────────
|
|
282
|
+
/**
|
|
283
|
+
* Paddle has no invoice-list endpoint: a **billed or completed transaction is** the
|
|
284
|
+
* invoice — it carries the `invoice_number` that is the legal record. So this lists the
|
|
285
|
+
* customer's billed and completed transactions and maps them onto {@link Invoice}.
|
|
286
|
+
*/
|
|
287
|
+
async listInvoices(customerId) {
|
|
288
|
+
const { data } = await this.#request(`/transactions?customer_id=${encodeURIComponent(customerId)}&status=billed,completed`);
|
|
289
|
+
return data.map((transaction) => {
|
|
290
|
+
const total = this.#toCents(transaction.details?.totals?.total) ?? 0;
|
|
291
|
+
return {
|
|
292
|
+
id: transaction.id,
|
|
293
|
+
gatewayId: transaction.id,
|
|
294
|
+
provider: this.provider,
|
|
295
|
+
...(transaction.customer_id ? { customerId: transaction.customer_id } : {}),
|
|
296
|
+
...(transaction.subscription_id ? { subscriptionId: transaction.subscription_id } : {}),
|
|
297
|
+
status: transaction.status === 'completed' ? 'paid' : 'open',
|
|
298
|
+
amount: { amount: total, currency: transaction.currency_code.toLowerCase() },
|
|
299
|
+
createdAt: transaction.created_at ?? new Date().toISOString(),
|
|
300
|
+
...(transaction.invoice_number ? { number: transaction.invoice_number } : {}),
|
|
301
|
+
...(transaction.billed_at ? { issuedAt: transaction.billed_at } : {}),
|
|
302
|
+
payload: transaction,
|
|
303
|
+
};
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
// ── Webhooks ─────────────────────────────────────────────────────────────────────────
|
|
307
|
+
/**
|
|
308
|
+
* Whether a delivery to `POST /payments/webhook/:provider` can be authenticated.
|
|
309
|
+
*
|
|
310
|
+
* `parseWebhook` already refuses without the secret — declaring it moves the refusal to boot.
|
|
311
|
+
*/
|
|
312
|
+
get webhookVerification() {
|
|
313
|
+
return this.#webhookSecret !== undefined ? 'configured' : 'unconfigured';
|
|
314
|
+
}
|
|
315
|
+
parseWebhook(rawBody, headers) {
|
|
316
|
+
if (this.#webhookSecret === undefined) {
|
|
317
|
+
throw new Error('[payments] Paddle webhook processing requires the notification setting secret ' +
|
|
318
|
+
'(`pdl_ntfset_…`). Set `PADDLE_WEBHOOK_SECRET` or pass `webhookSecret` to ' +
|
|
319
|
+
'`payments.paddle()`.');
|
|
320
|
+
}
|
|
321
|
+
const header = headerValue(headers, 'paddle-signature');
|
|
322
|
+
if (header === undefined || header === '') {
|
|
323
|
+
throw new Error('[payments] Missing `Paddle-Signature` header on webhook request.');
|
|
324
|
+
}
|
|
325
|
+
const { ts, h1 } = this.#parseSignatureHeader(header);
|
|
326
|
+
// Paddle signs `<ts>:<raw body>` with HMAC-SHA256 and sends the digest as hex. The
|
|
327
|
+
// shared `verifyHmacSignature` helper digests to base64, so it does not fit here.
|
|
328
|
+
const expected = createHmac('sha256', this.#webhookSecret)
|
|
329
|
+
.update(`${ts}:${rawBody}`, 'utf8')
|
|
330
|
+
.digest('hex');
|
|
331
|
+
if (!safeCompare(h1, expected)) {
|
|
332
|
+
throw new Error('[payments] Invalid Paddle webhook signature.');
|
|
333
|
+
}
|
|
334
|
+
// Replay protection is opt-in. Paddle documents the timestamp check as optional and
|
|
335
|
+
// its own SDK rejects anything older than five seconds — a default that tight would
|
|
336
|
+
// drop real money events on a retry or a skewed clock, and the billing layer already
|
|
337
|
+
// deduplicates on `event_id`.
|
|
338
|
+
if (this.#webhookMaxAgeSeconds !== undefined) {
|
|
339
|
+
const age = Math.abs(Date.now() / 1000 - Number(ts));
|
|
340
|
+
if (!Number.isFinite(age) || age > this.#webhookMaxAgeSeconds) {
|
|
341
|
+
throw new Error('[payments] Paddle webhook timestamp is outside the accepted window.');
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
const payload = JSON.parse(rawBody);
|
|
345
|
+
// Mapped once and passed down: the normalized type decides which dispute fields the
|
|
346
|
+
// payload carries, so deriving it twice invites the two halves to disagree.
|
|
347
|
+
const type = this.#mapWebhookType(payload.event_type, payload.data);
|
|
348
|
+
return {
|
|
349
|
+
id: payload.event_id,
|
|
350
|
+
provider: this.provider,
|
|
351
|
+
type,
|
|
352
|
+
...(payload.occurred_at !== undefined ? { createdAt: payload.occurred_at } : {}),
|
|
353
|
+
data: this.#mapWebhookData(payload, type),
|
|
354
|
+
raw: payload,
|
|
355
|
+
};
|
|
356
|
+
}
|
|
357
|
+
// ── Mappers ──────────────────────────────────────────────────────────────────────────
|
|
358
|
+
#mapCustomer(data) {
|
|
359
|
+
return {
|
|
360
|
+
id: data.id,
|
|
361
|
+
email: data.email,
|
|
362
|
+
...(data.name ? { name: data.name } : {}),
|
|
363
|
+
...(data.custom_data ? { metadata: data.custom_data } : {}),
|
|
364
|
+
};
|
|
365
|
+
}
|
|
366
|
+
#mapPayment(data) {
|
|
367
|
+
const statusMap = {
|
|
368
|
+
draft: 'pending',
|
|
369
|
+
ready: 'pending',
|
|
370
|
+
billed: 'pending',
|
|
371
|
+
paid: 'paid',
|
|
372
|
+
completed: 'paid',
|
|
373
|
+
canceled: 'canceled',
|
|
374
|
+
past_due: 'failed',
|
|
375
|
+
};
|
|
376
|
+
// `total` is what this transaction is worth. `grand_total` can differ once credits or
|
|
377
|
+
// a customer balance are involved; it stays available on `payment.payload`.
|
|
378
|
+
const total = this.#toCents(data.details?.totals?.total) ?? 0;
|
|
379
|
+
const result = {
|
|
380
|
+
id: data.id,
|
|
381
|
+
gatewayId: data.id,
|
|
382
|
+
provider: this.provider,
|
|
383
|
+
amount: { amount: total, currency: data.currency_code.toLowerCase() },
|
|
384
|
+
status: statusMap[data.status] ?? 'pending',
|
|
385
|
+
payload: data,
|
|
386
|
+
createdAt: data.created_at ?? new Date().toISOString(),
|
|
387
|
+
};
|
|
388
|
+
if (data.customer_id)
|
|
389
|
+
result.customerId = data.customer_id;
|
|
390
|
+
if (data.subscription_id)
|
|
391
|
+
result.subscriptionId = data.subscription_id;
|
|
392
|
+
if (data.checkout?.url)
|
|
393
|
+
result.hostedUrl = data.checkout.url;
|
|
394
|
+
// Paddle reports the instrument only after an attempt. Every one of them now has a
|
|
395
|
+
// category name, so PayPal, Apple Pay, iDEAL and the rest stop collapsing into silence.
|
|
396
|
+
const method = this.#mapMethodType(data.payments?.[0]?.method_details?.type);
|
|
397
|
+
if (method !== undefined)
|
|
398
|
+
result.method = method;
|
|
399
|
+
// Paddle has no "paid at": `billed_at` is when the invoice was raised, which for an
|
|
400
|
+
// automatically-collected transaction is the moment it was paid.
|
|
401
|
+
if ((data.status === 'paid' || data.status === 'completed') && data.billed_at) {
|
|
402
|
+
result.paidAt = data.billed_at;
|
|
403
|
+
}
|
|
404
|
+
return result;
|
|
405
|
+
}
|
|
406
|
+
#mapSubscription(data) {
|
|
407
|
+
const statusMap = {
|
|
408
|
+
active: 'active',
|
|
409
|
+
trialing: 'trialing',
|
|
410
|
+
past_due: 'past_due',
|
|
411
|
+
canceled: 'canceled',
|
|
412
|
+
// A paused Paddle subscription still exists and will bill again — but it is not
|
|
413
|
+
// billing NOW, so reporting it as `active` entitled a subscriber who is not paying.
|
|
414
|
+
paused: 'paused',
|
|
415
|
+
};
|
|
416
|
+
const item = data.items?.[0];
|
|
417
|
+
const unitAmount = this.#toCents(item?.price?.unit_price?.amount);
|
|
418
|
+
return {
|
|
419
|
+
id: data.id,
|
|
420
|
+
gatewayId: data.id,
|
|
421
|
+
provider: this.provider,
|
|
422
|
+
customerId: data.customer_id,
|
|
423
|
+
status: statusMap[data.status] ?? 'active',
|
|
424
|
+
planId: item?.price?.id ?? '',
|
|
425
|
+
...(unitAmount !== undefined
|
|
426
|
+
? {
|
|
427
|
+
amount: {
|
|
428
|
+
amount: unitAmount,
|
|
429
|
+
currency: (item?.price?.unit_price?.currency_code ??
|
|
430
|
+
data.currency_code ??
|
|
431
|
+
this.#currency).toLowerCase(),
|
|
432
|
+
},
|
|
433
|
+
}
|
|
434
|
+
: {}),
|
|
435
|
+
...(data.trial_dates?.ends_at ? { trialEndsAt: data.trial_dates.ends_at } : {}),
|
|
436
|
+
...(data.canceled_at ? { endsAt: data.canceled_at } : {}),
|
|
437
|
+
...(data.current_billing_period?.starts_at
|
|
438
|
+
? { currentPeriodStart: data.current_billing_period.starts_at }
|
|
439
|
+
: {}),
|
|
440
|
+
...(data.current_billing_period?.ends_at
|
|
441
|
+
? { currentPeriodEnd: data.current_billing_period.ends_at }
|
|
442
|
+
: {}),
|
|
443
|
+
payload: data,
|
|
444
|
+
createdAt: data.created_at ?? new Date().toISOString(),
|
|
445
|
+
};
|
|
446
|
+
}
|
|
447
|
+
#mapWebhookType(eventType, data) {
|
|
448
|
+
switch (eventType) {
|
|
449
|
+
case 'transaction.completed':
|
|
450
|
+
case 'transaction.paid':
|
|
451
|
+
return 'payment.succeeded';
|
|
452
|
+
case 'transaction.payment_failed':
|
|
453
|
+
return 'payment.failed';
|
|
454
|
+
case 'transaction.created':
|
|
455
|
+
case 'transaction.updated':
|
|
456
|
+
case 'transaction.ready':
|
|
457
|
+
case 'transaction.billed':
|
|
458
|
+
case 'transaction.canceled':
|
|
459
|
+
case 'transaction.past_due':
|
|
460
|
+
case 'transaction.revised':
|
|
461
|
+
return 'payment.updated';
|
|
462
|
+
case 'adjustment.created':
|
|
463
|
+
case 'adjustment.updated': {
|
|
464
|
+
// Paddle has no `dispute.*` event — checked against the current event-type
|
|
465
|
+
// reference, not from memory. A chargeback IS an adjustment, and `action` is the
|
|
466
|
+
// whole dispute vocabulary. (`developer.paddle.com/webhook-reference/
|
|
467
|
+
// risk-dispute-alerts/*` is Paddle CLASSIC; this driver is Paddle Billing.)
|
|
468
|
+
if (data.action === 'refund')
|
|
469
|
+
return 'payment.refunded';
|
|
470
|
+
// Only the CREATED event is a dispute moment. `adjustment.updated` fires for the
|
|
471
|
+
// approval lifecycle of an adjustment that already exists, and a second
|
|
472
|
+
// `payment.disputed` for the same chargeback is noise.
|
|
473
|
+
if (eventType === 'adjustment.created') {
|
|
474
|
+
const type = this.#disputeType(data.action);
|
|
475
|
+
if (type !== undefined)
|
|
476
|
+
return type;
|
|
477
|
+
}
|
|
478
|
+
// `credit`, `credit_reverse`, and every later status change on a chargeback
|
|
479
|
+
// adjustment.
|
|
480
|
+
return 'payment.updated';
|
|
481
|
+
}
|
|
482
|
+
case 'subscription.created':
|
|
483
|
+
case 'subscription.imported':
|
|
484
|
+
return 'subscription.created';
|
|
485
|
+
case 'subscription.activated':
|
|
486
|
+
case 'subscription.updated':
|
|
487
|
+
case 'subscription.trialing':
|
|
488
|
+
case 'subscription.past_due':
|
|
489
|
+
case 'subscription.paused':
|
|
490
|
+
case 'subscription.resumed':
|
|
491
|
+
return 'subscription.updated';
|
|
492
|
+
case 'subscription.canceled':
|
|
493
|
+
return 'subscription.canceled';
|
|
494
|
+
default:
|
|
495
|
+
return eventType;
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
/**
|
|
499
|
+
* An adjustment `action` → the canonical dispute type, or `undefined` when the action
|
|
500
|
+
* is not part of the dispute family.
|
|
501
|
+
*
|
|
502
|
+
* **`chargeback_warning` is not a funds-untouched warning.** That is the whole finding
|
|
503
|
+
* here, and it runs the opposite way to Stripe's inquiry and Adyen's
|
|
504
|
+
* `NOTIFICATION_OF_CHARGEBACK`, both of which leave the money in the account. Paddle is
|
|
505
|
+
* the merchant of record, so it does not tell you a chargeback is coming and wait for
|
|
506
|
+
* you to act — it acts. Paddle's own words: "If an early-stage dispute is detected, a
|
|
507
|
+
* `chargeback_warning` adjustment is created. The disputed amount is refunded, and a
|
|
508
|
+
* service fee is applied", and an adjustment is a money movement by definition —
|
|
509
|
+
* `chargeback_reverse` exists to "return the amount **held**".
|
|
510
|
+
*
|
|
511
|
+
* So both `chargeback` and `chargeback_warning` are `payment.disputed`: the revenue is
|
|
512
|
+
* gone in both, and mapping the warning to `payment.dispute_warning` — which writes
|
|
513
|
+
* nothing — would leave a row saying `paid` over money Paddle has already refunded to
|
|
514
|
+
* the buyer. Paddle sends no funds-untouched pre-dispute notification at all, and this
|
|
515
|
+
* driver does not invent one.
|
|
516
|
+
*
|
|
517
|
+
* The `*_reverse` pair is the money coming back. Paddle spells it out for
|
|
518
|
+
* `chargeback_reverse` ("Where a chargeback is contested successfully, Paddle creates an
|
|
519
|
+
* adjustment with the type `chargeback_reverse` to return the amount held"); for
|
|
520
|
+
* `chargeback_warning_reverse` the reference says only "Reversal of a chargeback
|
|
521
|
+
* warning", so `won` is read from the reversal symmetry Paddle's own naming sets up —
|
|
522
|
+
* every `*_reverse` action undoes its counterpart. Both are `won` because both put the
|
|
523
|
+
* amount back, and leaving the row at `disputed` would write off money that returned.
|
|
524
|
+
*/
|
|
525
|
+
#disputeType(action) {
|
|
526
|
+
if (action === 'chargeback' || action === 'chargeback_warning')
|
|
527
|
+
return 'payment.disputed';
|
|
528
|
+
if (action === 'chargeback_reverse' || action === 'chargeback_warning_reverse') {
|
|
529
|
+
return 'payment.dispute_closed';
|
|
530
|
+
}
|
|
531
|
+
return undefined;
|
|
532
|
+
}
|
|
533
|
+
/**
|
|
534
|
+
* The fields that make a dispute event actionable, added only to the dispute types.
|
|
535
|
+
*
|
|
536
|
+
* There is deliberately no `actionableUntil`: Paddle accepts no evidence from sellers,
|
|
537
|
+
* so there is no response window that belongs to you, and no adjustment field carries
|
|
538
|
+
* one. The deadline on a Paddle chargeback is Paddle's, and the honest normalized event
|
|
539
|
+
* is one that does not pretend otherwise.
|
|
540
|
+
*/
|
|
541
|
+
#disputeExtras(adjustment, type) {
|
|
542
|
+
if (type !== 'payment.disputed' && type !== 'payment.dispute_closed')
|
|
543
|
+
return {};
|
|
544
|
+
return {
|
|
545
|
+
// The adjustment IS the dispute here; Paddle has no separate dispute resource.
|
|
546
|
+
disputeId: adjustment.id,
|
|
547
|
+
...(adjustment.reason ? { reason: adjustment.reason } : {}),
|
|
548
|
+
...(type === 'payment.dispute_closed' ? { outcome: 'won' } : {}),
|
|
549
|
+
};
|
|
550
|
+
}
|
|
551
|
+
#mapWebhookData(payload, type) {
|
|
552
|
+
const data = payload.data;
|
|
553
|
+
const externalReference = this.#readExternalReference(data.custom_data);
|
|
554
|
+
if (payload.event_type.startsWith('subscription.')) {
|
|
555
|
+
const subscription = this.#mapSubscription(data);
|
|
556
|
+
return {
|
|
557
|
+
gatewayId: subscription.gatewayId,
|
|
558
|
+
customerId: subscription.customerId,
|
|
559
|
+
status: subscription.status,
|
|
560
|
+
planId: subscription.planId,
|
|
561
|
+
...(subscription.endsAt !== undefined ? { endsAt: subscription.endsAt } : {}),
|
|
562
|
+
...(externalReference !== undefined ? { externalReference } : {}),
|
|
563
|
+
};
|
|
564
|
+
}
|
|
565
|
+
if (payload.event_type.startsWith('adjustment.')) {
|
|
566
|
+
const adjustment = data;
|
|
567
|
+
return {
|
|
568
|
+
gatewayId: adjustment.transaction_id,
|
|
569
|
+
adjustmentId: adjustment.id,
|
|
570
|
+
amount: this.#toCents(adjustment.totals?.total) ?? 0,
|
|
571
|
+
currency: (adjustment.totals?.currency_code ?? this.#currency).toLowerCase(),
|
|
572
|
+
...(externalReference !== undefined ? { externalReference } : {}),
|
|
573
|
+
...this.#disputeExtras(adjustment, type),
|
|
574
|
+
};
|
|
575
|
+
}
|
|
576
|
+
const payment = this.#mapPayment(data);
|
|
577
|
+
return {
|
|
578
|
+
gatewayId: payment.gatewayId,
|
|
579
|
+
amount: payment.amount.amount,
|
|
580
|
+
currency: payment.amount.currency,
|
|
581
|
+
...(payment.customerId !== undefined ? { customerId: payment.customerId } : {}),
|
|
582
|
+
...(payment.subscriptionId !== undefined ? { subscriptionId: payment.subscriptionId } : {}),
|
|
583
|
+
...(externalReference !== undefined ? { externalReference } : {}),
|
|
584
|
+
};
|
|
585
|
+
}
|
|
586
|
+
// ── Helpers ──────────────────────────────────────────────────────────────────────────
|
|
587
|
+
#catalogItem(input) {
|
|
588
|
+
return [{ price_id: input.planId, quantity: 1 }];
|
|
589
|
+
}
|
|
590
|
+
#adHocItem(input, currency) {
|
|
591
|
+
if (this.#productId === undefined) {
|
|
592
|
+
throw new Error('[payments] Paddle needs a product for a non-catalog checkout. Pass `planId` (a ' +
|
|
593
|
+
'Paddle price id) on the checkout, or set `productId` in ' +
|
|
594
|
+
'`payments.paddle({ productId })` / `PADDLE_PRODUCT_ID`.');
|
|
595
|
+
}
|
|
596
|
+
return [
|
|
597
|
+
{
|
|
598
|
+
quantity: 1,
|
|
599
|
+
price: {
|
|
600
|
+
description: input.description ?? 'Payment',
|
|
601
|
+
product_id: this.#productId,
|
|
602
|
+
// Paddle's money is a string of the **smallest unit** — `"1990"` is $19.90, not
|
|
603
|
+
// $1990. It reads like a decimal and is not one, so this is the package's own
|
|
604
|
+
// integer stringified: no `formatDecimal`, no `toDecimal`, no division. (Paddle
|
|
605
|
+
// API reference, "Data types": "Monetary values are returned as strings in the
|
|
606
|
+
// lowest denomination for a currency.")
|
|
607
|
+
unit_price: { amount: String(input.amount), currency_code: currency },
|
|
608
|
+
},
|
|
609
|
+
},
|
|
610
|
+
];
|
|
611
|
+
}
|
|
612
|
+
/**
|
|
613
|
+
* The routing key is written to `custom_data.external_reference`, which Paddle echoes on
|
|
614
|
+
* the transaction and on the subscription a recurring checkout creates — and which
|
|
615
|
+
* `parseWebhook` reads back out. `metadata.externalReference` is still honoured as a
|
|
616
|
+
* fallback for callers written before `CheckoutInput.externalReference` existed.
|
|
617
|
+
*/
|
|
618
|
+
#customData(input) {
|
|
619
|
+
const { checkoutUrl: _checkoutUrl, externalReference: fromMetadata, ...rest } = input.metadata ?? {};
|
|
620
|
+
const reference = input.externalReference ?? (typeof fromMetadata === 'string' ? fromMetadata : undefined);
|
|
621
|
+
const custom = {
|
|
622
|
+
...rest,
|
|
623
|
+
...(reference !== undefined ? { external_reference: reference } : {}),
|
|
624
|
+
};
|
|
625
|
+
return Object.keys(custom).length > 0 ? custom : undefined;
|
|
626
|
+
}
|
|
627
|
+
/** `custom_data.external_reference` is where this driver puts the app's routing key. */
|
|
628
|
+
#readExternalReference(customData) {
|
|
629
|
+
const value = customData?.external_reference;
|
|
630
|
+
return typeof value === 'string' ? value : undefined;
|
|
631
|
+
}
|
|
632
|
+
/**
|
|
633
|
+
* Paddle keeps tax identifiers on the customer's *business* — a separate resource with
|
|
634
|
+
* its own endpoints — so a tax id written onto the customer would silently vanish.
|
|
635
|
+
*/
|
|
636
|
+
#refuseTaxId(taxId) {
|
|
637
|
+
if (taxId === undefined)
|
|
638
|
+
return;
|
|
639
|
+
throw new Error('[payments] Paddle stores tax ids on a business (`/customers/{id}/businesses`), ' +
|
|
640
|
+
'not on the customer. Create or update the business directly.');
|
|
641
|
+
}
|
|
642
|
+
/**
|
|
643
|
+
* Paddle's `payments[].method_details.type` onto the contract's method **categories**.
|
|
644
|
+
*
|
|
645
|
+
* Paddle collects through a dozen local methods and only `card` used to have a name
|
|
646
|
+
* here, so a PayPal or iDEAL payment came back with no `method` at all. The categories
|
|
647
|
+
* close that: PayPal, the device wallets and the Asian super-app wallets are `wallet`;
|
|
648
|
+
* iDEAL, Bancontact, BLIK, MB WAY and a wire are `bank_transfer`; Pix and UPI are
|
|
649
|
+
* themselves. Paddle spells the same value hyphenated on some events (`apple-pay`) and
|
|
650
|
+
* underscored on others, so both are normalized.
|
|
651
|
+
*/
|
|
652
|
+
#mapMethodType(type) {
|
|
653
|
+
if (type === undefined)
|
|
654
|
+
return undefined;
|
|
655
|
+
switch (type.replace(/-/g, '_')) {
|
|
656
|
+
case 'card':
|
|
657
|
+
case 'korea_local':
|
|
658
|
+
case 'south_korea_local_card':
|
|
659
|
+
return 'card';
|
|
660
|
+
case 'paypal':
|
|
661
|
+
case 'apple_pay':
|
|
662
|
+
case 'google_pay':
|
|
663
|
+
case 'samsung_pay':
|
|
664
|
+
case 'alipay':
|
|
665
|
+
case 'wechat_pay':
|
|
666
|
+
case 'kakao_pay':
|
|
667
|
+
case 'naver_pay':
|
|
668
|
+
case 'payco':
|
|
669
|
+
return 'wallet';
|
|
670
|
+
case 'ideal':
|
|
671
|
+
case 'bancontact':
|
|
672
|
+
case 'blik':
|
|
673
|
+
case 'mb_way':
|
|
674
|
+
case 'wire_transfer':
|
|
675
|
+
return 'bank_transfer';
|
|
676
|
+
case 'pix':
|
|
677
|
+
return 'pix';
|
|
678
|
+
case 'upi':
|
|
679
|
+
return 'upi';
|
|
680
|
+
default:
|
|
681
|
+
// `offline`, `unknown`, and whatever Paddle adds next. Leaving `method` unset says
|
|
682
|
+
// "Paddle did not tell us"; `'unknown'` would claim it did and the answer was none.
|
|
683
|
+
return undefined;
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
/**
|
|
687
|
+
* Paddle has **no request deduplication of any kind** — no `Idempotency-Key` header, no
|
|
688
|
+
* request-id body field, nothing on a transaction or an adjustment. So a key handed to
|
|
689
|
+
* this driver is refused rather than accepted and dropped: silently dropping it turns a
|
|
690
|
+
* caller's retry guarantee into a second refund.
|
|
691
|
+
*/
|
|
692
|
+
#refuseIdempotencyKey(key, operation) {
|
|
693
|
+
if (key === undefined)
|
|
694
|
+
return;
|
|
695
|
+
throw new Error(`[payments] Paddle has no idempotency mechanism, so \`idempotencyKey\` cannot be honoured on ${operation}(). Paddle's API deduplicates nothing, and a retried request performs the operation a second time — deduplicate on your side (persist the key and check it) before calling.`);
|
|
696
|
+
}
|
|
697
|
+
#mapCheckoutStatus(status) {
|
|
698
|
+
if (status === 'completed' || status === 'paid')
|
|
699
|
+
return 'complete';
|
|
700
|
+
if (status === 'canceled')
|
|
701
|
+
return 'expired';
|
|
702
|
+
return 'open';
|
|
703
|
+
}
|
|
704
|
+
/** Paddle money is a string of the smallest unit; this package's `Money` is that integer. */
|
|
705
|
+
#toCents(value) {
|
|
706
|
+
if (value === null || value === undefined || value === '')
|
|
707
|
+
return undefined;
|
|
708
|
+
const parsed = Number(value);
|
|
709
|
+
return Number.isFinite(parsed) ? parsed : undefined;
|
|
710
|
+
}
|
|
711
|
+
#parseSignatureHeader(header) {
|
|
712
|
+
const parts = new Map();
|
|
713
|
+
for (const segment of header.split(';')) {
|
|
714
|
+
const index = segment.indexOf('=');
|
|
715
|
+
if (index > 0)
|
|
716
|
+
parts.set(segment.slice(0, index).trim(), segment.slice(index + 1).trim());
|
|
717
|
+
}
|
|
718
|
+
const ts = parts.get('ts');
|
|
719
|
+
const h1 = parts.get('h1');
|
|
720
|
+
if (ts === undefined || h1 === undefined) {
|
|
721
|
+
throw new Error('[payments] Malformed `Paddle-Signature` header — expected `ts=…;h1=…`.');
|
|
722
|
+
}
|
|
723
|
+
return { ts, h1 };
|
|
724
|
+
}
|
|
725
|
+
async #request(path, options = {}) {
|
|
726
|
+
return httpRequest(path, {
|
|
727
|
+
baseUrl: this.#baseUrl,
|
|
728
|
+
...(options.method !== undefined ? { method: options.method } : {}),
|
|
729
|
+
...(options.body !== undefined ? { body: options.body } : {}),
|
|
730
|
+
bearerToken: this.#apiKey,
|
|
731
|
+
// The shared helper has no generic header option, so the raw-header slot pins the
|
|
732
|
+
// API version — Paddle applies the account's default version without it, which is
|
|
733
|
+
// whatever a future breaking change makes it.
|
|
734
|
+
authHeader: { name: 'Paddle-Version', value: '1' },
|
|
735
|
+
});
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
//# sourceMappingURL=paddle.js.map
|