@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,905 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { publishPaymentDiagnostics, publishRefundDiagnostics, publishSubscriptionDiagnostics, } from '../diagnostics.js';
|
|
3
|
+
import { headerValue, httpRequest, isNotFound } from '../http.js';
|
|
4
|
+
import { emitInvoiceIfRequested } from '../invoice/emit_invoice.js';
|
|
5
|
+
import { verifyHmacOverPayload } from '../webhook_security.js';
|
|
6
|
+
import { requireCredential, requireCurrency } from './shared.js';
|
|
7
|
+
/** The `Square-Version` these mappings were written against. */
|
|
8
|
+
const DEFAULT_API_VERSION = '2026-08-19';
|
|
9
|
+
/** `idempotency_key` on the Payments and Refunds APIs. Payment links allow 192. */
|
|
10
|
+
const IDEMPOTENCY_MAX_LENGTH = 45;
|
|
11
|
+
/** `reference_id` on a Payment and on an Order. */
|
|
12
|
+
const REFERENCE_MAX_LENGTH = 40;
|
|
13
|
+
/** `note` on a Payment. */
|
|
14
|
+
const NOTE_MAX_LENGTH = 500;
|
|
15
|
+
/**
|
|
16
|
+
* Square driver — the US-first seller platform (`connect.squareup.com/v2`), Bearer token,
|
|
17
|
+
* multi-currency, everything scoped to one seller location.
|
|
18
|
+
*
|
|
19
|
+
* Three shapes of this API drive the mappings below:
|
|
20
|
+
*
|
|
21
|
+
* **A charge needs a token you cannot mint server-side.** `POST /v2/payments` takes a
|
|
22
|
+
* `source_id`: a single-use card nonce from the Web Payments SDK, or a `ccof:`/card-on-file
|
|
23
|
+
* id. There is no "charge this customer for this amount" call, so `charge()` requires
|
|
24
|
+
* `paymentMethodId` (or `card.token`) and refuses without one rather than inventing a flow.
|
|
25
|
+
*
|
|
26
|
+
* **`idempotency_key` is a body field, not a header** — on payments, refunds, subscriptions
|
|
27
|
+
* and payment links alike. Square also *requires* it on payments and refunds, so a call
|
|
28
|
+
* that arrives without one gets a generated UUID; that protects the retry inside a single
|
|
29
|
+
* call, not a retry from your own job queue, which is what `idempotencyKey` is for.
|
|
30
|
+
*
|
|
31
|
+
* **Webhook signatures cover `notificationUrl + body`.** The URL is part of the signed
|
|
32
|
+
* material, which means the driver has to be told its own public URL. Configure
|
|
33
|
+
* `webhookSignatureKey` without `notificationUrl` and the constructor throws: the
|
|
34
|
+
* alternative is a driver that rejects every genuine webhook, or worse, one that decides
|
|
35
|
+
* to skip verification because it lacks a piece of the input.
|
|
36
|
+
*
|
|
37
|
+
* Money is an integer in the currency's minor unit on both sides (`{ amount: 1990,
|
|
38
|
+
* currency: 'USD' }`), so this driver converts nothing — see {@link SquareDriver.charge}.
|
|
39
|
+
*/
|
|
40
|
+
export class SquareDriver {
|
|
41
|
+
provider = 'square';
|
|
42
|
+
/**
|
|
43
|
+
* `source_id` decides the instrument, and it is minted in the browser — the driver hands
|
|
44
|
+
* Square an opaque token and learns what it was only from `source_type` on the way back.
|
|
45
|
+
* The Web Payments SDK mints more than card tokens, though: Cash App Pay and the device
|
|
46
|
+
* wallets (`WALLET`), ACH bank tokens (`BANK_ACCOUNT`) and Afterpay/Clearpay
|
|
47
|
+
* (`BUY_NOW_PAY_LATER`) all arrive at `POST /v2/payments` through the same `source_id`,
|
|
48
|
+
* so `charge()` genuinely produces those categories. `undefined` covers the hosted
|
|
49
|
+
* payment link, where the payer picks. Pix and boleto are absent because Square does not
|
|
50
|
+
* sell in Brazil.
|
|
51
|
+
*/
|
|
52
|
+
supportedMethods = [
|
|
53
|
+
'credit_card',
|
|
54
|
+
'debit_card',
|
|
55
|
+
'wallet',
|
|
56
|
+
'bank_debit',
|
|
57
|
+
'bnpl',
|
|
58
|
+
'undefined',
|
|
59
|
+
];
|
|
60
|
+
capabilities = { refunds: true, invoices: true, subscriptions: true };
|
|
61
|
+
#baseUrl;
|
|
62
|
+
#accessToken;
|
|
63
|
+
#locationId;
|
|
64
|
+
#currency;
|
|
65
|
+
#apiVersion;
|
|
66
|
+
#webhookSignatureKey;
|
|
67
|
+
#notificationUrl;
|
|
68
|
+
#invoiceCtx;
|
|
69
|
+
constructor(ctx, config) {
|
|
70
|
+
this.#invoiceCtx = ctx;
|
|
71
|
+
this.#accessToken = requireCredential({
|
|
72
|
+
driver: 'square',
|
|
73
|
+
option: 'accessToken',
|
|
74
|
+
env: 'SQUARE_ACCESS_TOKEN',
|
|
75
|
+
value: config.accessToken,
|
|
76
|
+
});
|
|
77
|
+
this.#locationId = requireCredential({
|
|
78
|
+
driver: 'square',
|
|
79
|
+
option: 'locationId',
|
|
80
|
+
env: 'SQUARE_LOCATION_ID',
|
|
81
|
+
value: config.locationId,
|
|
82
|
+
});
|
|
83
|
+
this.#currency = requireCurrency('square', config.currency);
|
|
84
|
+
this.#apiVersion = config.apiVersion ?? DEFAULT_API_VERSION;
|
|
85
|
+
this.#webhookSignatureKey =
|
|
86
|
+
config.webhookSignatureKey ?? process.env.SQUARE_WEBHOOK_SIGNATURE_KEY;
|
|
87
|
+
this.#notificationUrl = config.notificationUrl ?? process.env.SQUARE_WEBHOOK_NOTIFICATION_URL;
|
|
88
|
+
if (this.#webhookSignatureKey !== undefined && this.#notificationUrl === undefined) {
|
|
89
|
+
throw new Error('[payments] Driver "square" has a webhookSignatureKey but no notificationUrl. Square signs `notificationUrl + body`, so the URL is part of the secret: set `SQUARE_WEBHOOK_NOTIFICATION_URL` (or pass `notificationUrl`) to the exact URL registered on the webhook subscription.');
|
|
90
|
+
}
|
|
91
|
+
this.#baseUrl =
|
|
92
|
+
config.sandbox === true
|
|
93
|
+
? 'https://connect.squareupsandbox.com/v2'
|
|
94
|
+
: 'https://connect.squareup.com/v2';
|
|
95
|
+
}
|
|
96
|
+
// ── Customers ────────────────────────────────────────────────────────────────────────
|
|
97
|
+
async createCustomer(input) {
|
|
98
|
+
const body = {
|
|
99
|
+
idempotency_key: this.#idempotencyKey(input.idempotencyKey),
|
|
100
|
+
// Square splits a person into `given_name`/`family_name` and has no single "name"
|
|
101
|
+
// field. Splitting on whitespace guesses which token is the surname, which is wrong
|
|
102
|
+
// for most of the world's names, so the whole string goes in `given_name` and the
|
|
103
|
+
// caller can pass `metadata.familyName` when it genuinely knows the split.
|
|
104
|
+
...(input.name !== undefined ? { given_name: input.name } : {}),
|
|
105
|
+
...(typeof input.metadata?.familyName === 'string'
|
|
106
|
+
? { family_name: input.metadata.familyName }
|
|
107
|
+
: {}),
|
|
108
|
+
...(typeof input.metadata?.companyName === 'string'
|
|
109
|
+
? { company_name: input.metadata.companyName }
|
|
110
|
+
: {}),
|
|
111
|
+
...(input.email !== undefined ? { email_address: input.email } : {}),
|
|
112
|
+
...(typeof input.metadata?.phone === 'string' ? { phone_number: input.metadata.phone } : {}),
|
|
113
|
+
...(typeof input.metadata?.referenceId === 'string'
|
|
114
|
+
? { reference_id: input.metadata.referenceId }
|
|
115
|
+
: {}),
|
|
116
|
+
...this.#taxIds(input.taxId, input.metadata),
|
|
117
|
+
};
|
|
118
|
+
const data = await this.#request('/customers', {
|
|
119
|
+
method: 'POST',
|
|
120
|
+
body,
|
|
121
|
+
});
|
|
122
|
+
return this.#mapCustomer(data.customer);
|
|
123
|
+
}
|
|
124
|
+
async findCustomer(customerId) {
|
|
125
|
+
try {
|
|
126
|
+
const data = await this.#request(`/customers/${customerId}`);
|
|
127
|
+
return this.#mapCustomer(data.customer);
|
|
128
|
+
}
|
|
129
|
+
catch (error) {
|
|
130
|
+
if (isNotFound(error))
|
|
131
|
+
return null;
|
|
132
|
+
throw error;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
async updateCustomer(customerId, input) {
|
|
136
|
+
const body = {
|
|
137
|
+
...(input.name !== undefined ? { given_name: input.name } : {}),
|
|
138
|
+
...(typeof input.metadata?.familyName === 'string'
|
|
139
|
+
? { family_name: input.metadata.familyName }
|
|
140
|
+
: {}),
|
|
141
|
+
...(input.email !== undefined ? { email_address: input.email } : {}),
|
|
142
|
+
...(typeof input.metadata?.phone === 'string' ? { phone_number: input.metadata.phone } : {}),
|
|
143
|
+
...this.#taxIds(input.taxId, input.metadata),
|
|
144
|
+
};
|
|
145
|
+
const data = await this.#request(`/customers/${customerId}`, { method: 'PUT', body });
|
|
146
|
+
return this.#mapCustomer(data.customer);
|
|
147
|
+
}
|
|
148
|
+
// ── Payments ─────────────────────────────────────────────────────────────────────────
|
|
149
|
+
async charge(input) {
|
|
150
|
+
const sourceId = input.paymentMethodId ?? input.card?.token;
|
|
151
|
+
if (sourceId === undefined) {
|
|
152
|
+
throw new Error('[payments] Square needs a `source_id` to charge: a single-use card token from the Web Payments SDK, or a saved card id (`ccof:…`) together with `customerId`. Pass it as `paymentMethodId` (or `card.token`) — there is no server-side "charge this customer" call.');
|
|
153
|
+
}
|
|
154
|
+
if (input.method !== undefined &&
|
|
155
|
+
!['credit_card', 'debit_card', 'wallet', 'bank_debit', 'bnpl', 'undefined'].includes(input.method)) {
|
|
156
|
+
throw new Error(`[payments] Square has no "${input.method}" payment method. The instrument is fixed by the token in \`paymentMethodId\`, so \`method\` is a declaration, not an instruction: it may name a card (\`credit_card\`/\`debit_card\`), a wallet, \`bank_debit\` (ACH) or \`bnpl\` (Afterpay), and Square — not this call — decides which the token turns out to be.`);
|
|
157
|
+
}
|
|
158
|
+
if (input.split !== undefined && input.split.length > 0) {
|
|
159
|
+
throw new Error('[payments] Square does not split a payment across recipients. `app_fee_money` takes a single application fee for an OAuth-connected seller, which is not the same thing; pass it as `metadata.appFeeAmount` if that is what you want.');
|
|
160
|
+
}
|
|
161
|
+
const currency = (input.currency ?? this.#currency).toUpperCase();
|
|
162
|
+
const referenceId = this.#reference(input.externalReference, 'charge');
|
|
163
|
+
const body = {
|
|
164
|
+
source_id: sourceId,
|
|
165
|
+
// Square requires an idempotency key and takes it in the BODY, not a header. A
|
|
166
|
+
// generated UUID only makes Square's own retry safe; a caller that wants a job retry
|
|
167
|
+
// to be safe has to pass its own `idempotencyKey`.
|
|
168
|
+
idempotency_key: this.#idempotencyKey(input.idempotencyKey),
|
|
169
|
+
// No conversion anywhere in this driver: Square's `amount_money.amount` is already the
|
|
170
|
+
// integer minor unit, the same unit as this package's `Money`. The Brazilian drivers
|
|
171
|
+
// next door divide by 100 because their gateways want decimal reais — doing that here
|
|
172
|
+
// would bill a hundredth of the charge, and Square would accept it.
|
|
173
|
+
amount_money: { amount: input.amount, currency },
|
|
174
|
+
location_id: this.#locationId,
|
|
175
|
+
...(input.customerId !== undefined ? { customer_id: input.customerId } : {}),
|
|
176
|
+
...(referenceId !== undefined ? { reference_id: referenceId } : {}),
|
|
177
|
+
...(input.description !== undefined
|
|
178
|
+
? { note: input.description.slice(0, NOTE_MAX_LENGTH) }
|
|
179
|
+
: {}),
|
|
180
|
+
...(input.card?.holder?.email !== undefined
|
|
181
|
+
? { buyer_email_address: input.card.holder.email }
|
|
182
|
+
: {}),
|
|
183
|
+
// Delayed capture: `autocomplete: false` leaves the payment APPROVED (money held, not
|
|
184
|
+
// moved) until `completePayment` runs. See that method for why it is not on the
|
|
185
|
+
// driver contract.
|
|
186
|
+
...(input.metadata?.autocomplete === false ? { autocomplete: false } : {}),
|
|
187
|
+
...(typeof input.metadata?.appFeeAmount === 'number'
|
|
188
|
+
? { app_fee_money: { amount: input.metadata.appFeeAmount, currency } }
|
|
189
|
+
: {}),
|
|
190
|
+
};
|
|
191
|
+
const data = await this.#request('/payments', {
|
|
192
|
+
method: 'POST',
|
|
193
|
+
body,
|
|
194
|
+
});
|
|
195
|
+
const payment = this.#mapPayment(data.payment);
|
|
196
|
+
await emitInvoiceIfRequested(this.#invoiceCtx, input, payment, this);
|
|
197
|
+
publishPaymentDiagnostics(payment);
|
|
198
|
+
return payment;
|
|
199
|
+
}
|
|
200
|
+
async findPayment(gatewayId) {
|
|
201
|
+
try {
|
|
202
|
+
const data = await this.#request(`/payments/${gatewayId}`);
|
|
203
|
+
return this.#mapPayment(data.payment);
|
|
204
|
+
}
|
|
205
|
+
catch (error) {
|
|
206
|
+
if (isNotFound(error))
|
|
207
|
+
return null;
|
|
208
|
+
throw error;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
async refund(paymentGatewayId, amount, options) {
|
|
212
|
+
// `RefundPayment` has no "refund everything" mode — `amount_money` is mandatory. So a
|
|
213
|
+
// full refund is a read followed by a write, and the amount refunded is the amount
|
|
214
|
+
// Square reports for the payment, never a number this driver computed.
|
|
215
|
+
const target = amount !== undefined
|
|
216
|
+
? { amount, currency: this.#currency.toUpperCase() }
|
|
217
|
+
: await this.#paymentAmount(paymentGatewayId);
|
|
218
|
+
const data = await this.#request('/refunds', {
|
|
219
|
+
method: 'POST',
|
|
220
|
+
body: {
|
|
221
|
+
// Square requires the key and takes it in the BODY. Generating one made Square's
|
|
222
|
+
// own retry safe and a retried job double-refund; the caller's key is what makes
|
|
223
|
+
// the second call return the first refund instead of issuing another.
|
|
224
|
+
idempotency_key: this.#idempotencyKey(options?.idempotencyKey),
|
|
225
|
+
payment_id: paymentGatewayId,
|
|
226
|
+
amount_money: target,
|
|
227
|
+
},
|
|
228
|
+
});
|
|
229
|
+
const refund = {
|
|
230
|
+
id: data.refund.id,
|
|
231
|
+
gatewayId: data.refund.id,
|
|
232
|
+
provider: this.provider,
|
|
233
|
+
amount: {
|
|
234
|
+
amount: data.refund.amount_money?.amount ?? target.amount,
|
|
235
|
+
currency: (data.refund.amount_money?.currency ?? target.currency).toLowerCase(),
|
|
236
|
+
},
|
|
237
|
+
// `PENDING` is the usual first answer for a card refund; `refund.updated` carrying
|
|
238
|
+
// `COMPLETED` is the only thing that means the money left.
|
|
239
|
+
status: data.refund.status === 'COMPLETED'
|
|
240
|
+
? 'succeeded'
|
|
241
|
+
: data.refund.status === 'REJECTED' || data.refund.status === 'FAILED'
|
|
242
|
+
? 'failed'
|
|
243
|
+
: 'pending',
|
|
244
|
+
createdAt: data.refund.created_at ?? new Date().toISOString(),
|
|
245
|
+
};
|
|
246
|
+
publishRefundDiagnostics(refund);
|
|
247
|
+
return refund;
|
|
248
|
+
}
|
|
249
|
+
/**
|
|
250
|
+
* Capture a payment created with `metadata: { autocomplete: false }`.
|
|
251
|
+
*
|
|
252
|
+
* Not part of {@link PaymentsDriver}: the contract has one verb for taking money, and a
|
|
253
|
+
* driver-specific second half does not fit it. It is public because the alternative is
|
|
254
|
+
* worse — an `APPROVED` payment now reads as `authorized`, and Square voids the
|
|
255
|
+
* authorization on its own when `delay_duration` runs out, so something has to be able
|
|
256
|
+
* to finish the job.
|
|
257
|
+
*/
|
|
258
|
+
async completePayment(paymentGatewayId) {
|
|
259
|
+
const data = await this.#request(`/payments/${paymentGatewayId}/complete`, { method: 'POST', body: {} });
|
|
260
|
+
const payment = this.#mapPayment(data.payment);
|
|
261
|
+
publishPaymentDiagnostics(payment);
|
|
262
|
+
return payment;
|
|
263
|
+
}
|
|
264
|
+
// ── Checkout ─────────────────────────────────────────────────────────────────────────
|
|
265
|
+
async createCheckout(input) {
|
|
266
|
+
if (input.cancelUrl !== undefined) {
|
|
267
|
+
throw new Error('[payments] A Square payment link has one redirect (`checkout_options.redirect_url`) and no cancel URL — a buyer who abandons it stays on the link. Drop `cancelUrl` rather than letting it look configured.');
|
|
268
|
+
}
|
|
269
|
+
if (input.trialDays !== undefined) {
|
|
270
|
+
throw new Error('[payments] Square puts a free trial in the subscription plan variation as a free phase, not on the checkout. Remove `trialDays` and point `planId` at a variation whose first phase is free.');
|
|
271
|
+
}
|
|
272
|
+
const currency = (input.currency ?? this.#currency).toUpperCase();
|
|
273
|
+
const referenceId = this.#reference(input.externalReference, 'checkout');
|
|
274
|
+
const body = {
|
|
275
|
+
idempotency_key: this.#idempotencyKey(input.idempotencyKey),
|
|
276
|
+
// The `order` form rather than `quick_pay`, for one reason: a PaymentLink has no
|
|
277
|
+
// reference field of its own, and an Order has `reference_id` plus `metadata`. With
|
|
278
|
+
// `quick_pay` there is nowhere to put `externalReference` at all.
|
|
279
|
+
order: {
|
|
280
|
+
location_id: this.#locationId,
|
|
281
|
+
line_items: [
|
|
282
|
+
{
|
|
283
|
+
name: input.description ?? 'Payment',
|
|
284
|
+
// Square wants the quantity as a string.
|
|
285
|
+
quantity: '1',
|
|
286
|
+
base_price_money: { amount: input.amount, currency },
|
|
287
|
+
},
|
|
288
|
+
],
|
|
289
|
+
...(input.customerId !== undefined ? { customer_id: input.customerId } : {}),
|
|
290
|
+
...(referenceId !== undefined ? { reference_id: referenceId } : {}),
|
|
291
|
+
...(referenceId !== undefined || input.metadata !== undefined
|
|
292
|
+
? { metadata: this.#orderMetadata(referenceId, input.metadata) }
|
|
293
|
+
: {}),
|
|
294
|
+
},
|
|
295
|
+
checkout_options: {
|
|
296
|
+
redirect_url: input.successUrl,
|
|
297
|
+
// A plan VARIATION id, not a plan id: Square subscribes buyers to variations.
|
|
298
|
+
...(input.planId !== undefined ? { subscription_plan_id: input.planId } : {}),
|
|
299
|
+
},
|
|
300
|
+
// Also carried onto the resulting Payment's `note`, which is the one place a
|
|
301
|
+
// reference survives into `payment.created`. See the docs page for why both.
|
|
302
|
+
...(referenceId !== undefined ? { payment_note: referenceId } : {}),
|
|
303
|
+
};
|
|
304
|
+
const data = await this.#request('/online-checkout/payment-links', { method: 'POST', body });
|
|
305
|
+
return {
|
|
306
|
+
id: data.payment_link.id,
|
|
307
|
+
gatewayId: data.payment_link.id,
|
|
308
|
+
provider: this.provider,
|
|
309
|
+
url: data.payment_link.url,
|
|
310
|
+
// Square has no status on a payment link — it either exists or it does not.
|
|
311
|
+
status: 'open',
|
|
312
|
+
amount: { amount: input.amount, currency: currency.toLowerCase() },
|
|
313
|
+
...(input.customerId !== undefined ? { customerId: input.customerId } : {}),
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
// ── Subscriptions ────────────────────────────────────────────────────────────────────
|
|
317
|
+
async createSubscription(input) {
|
|
318
|
+
if (input.cycle !== undefined) {
|
|
319
|
+
throw new Error('[payments] Square takes the billing cadence from the subscription plan variation, not from the call. Remove `cycle` and point `planId` at a variation with that cadence.');
|
|
320
|
+
}
|
|
321
|
+
if (input.method !== undefined) {
|
|
322
|
+
throw new Error('[payments] Square bills a subscription from a card on file or by emailing an invoice; there is no method to name. Remove `method` and pass `metadata.cardId` for the card-on-file flow.');
|
|
323
|
+
}
|
|
324
|
+
if (input.trialDays !== undefined) {
|
|
325
|
+
throw new Error('[payments] Square expresses a trial as a free phase on the plan variation, not as a number of days on the subscription. Remove `trialDays` and use a variation whose first phase is free.');
|
|
326
|
+
}
|
|
327
|
+
if (input.card !== undefined) {
|
|
328
|
+
throw new Error('[payments] A Web Payments SDK card token is single-use and cannot become a subscription card. Save it first with `POST /v2/cards`, then pass the resulting `card_…` id as `metadata.cardId`.');
|
|
329
|
+
}
|
|
330
|
+
if (input.externalReference !== undefined) {
|
|
331
|
+
throw new Error('[payments] A Square subscription has no reference or metadata field, so `externalReference` would be silently dropped and could never come back on `subscription.updated`. Key your record on `customerId` plus the returned subscription id instead.');
|
|
332
|
+
}
|
|
333
|
+
const body = {
|
|
334
|
+
idempotency_key: this.#idempotencyKey(input.idempotencyKey),
|
|
335
|
+
location_id: this.#locationId,
|
|
336
|
+
plan_variation_id: input.planId,
|
|
337
|
+
customer_id: input.customerId,
|
|
338
|
+
...(input.startDate !== undefined ? { start_date: this.#startDate(input.startDate) } : {}),
|
|
339
|
+
...(typeof input.metadata?.cardId === 'string' ? { card_id: input.metadata.cardId } : {}),
|
|
340
|
+
...(typeof input.metadata?.timezone === 'string'
|
|
341
|
+
? { timezone: input.metadata.timezone }
|
|
342
|
+
: {}),
|
|
343
|
+
// A real Square field, and the one place the contract's `amount` fits: it overrides
|
|
344
|
+
// the variation's price for a statically-priced plan. Minor units, no conversion.
|
|
345
|
+
...(input.amount !== undefined
|
|
346
|
+
? {
|
|
347
|
+
price_override_money: {
|
|
348
|
+
amount: input.amount,
|
|
349
|
+
currency: this.#currency.toUpperCase(),
|
|
350
|
+
},
|
|
351
|
+
}
|
|
352
|
+
: {}),
|
|
353
|
+
};
|
|
354
|
+
const data = await this.#request('/subscriptions', { method: 'POST', body });
|
|
355
|
+
const subscription = this.#mapSubscription(data.subscription, input.customerId);
|
|
356
|
+
publishSubscriptionDiagnostics(subscription, 'subscription.created');
|
|
357
|
+
return subscription;
|
|
358
|
+
}
|
|
359
|
+
async cancelSubscription(subscriptionGatewayId, options) {
|
|
360
|
+
if (options?.atPeriodEnd === false) {
|
|
361
|
+
throw new Error('[payments] Square cannot end a subscription mid-period: `POST /v2/subscriptions/{id}/cancel` sets `canceled_date` to the end of the current billing period and the status stays ACTIVE until then. Call it without `atPeriodEnd: false`, and refund the last invoice if the buyer should not have been billed for the period.');
|
|
362
|
+
}
|
|
363
|
+
const data = await this.#request(`/subscriptions/${subscriptionGatewayId}/cancel`, { method: 'POST', body: {} });
|
|
364
|
+
const subscription = this.#mapSubscription(data.subscription);
|
|
365
|
+
publishSubscriptionDiagnostics(subscription, 'subscription.canceled');
|
|
366
|
+
return subscription;
|
|
367
|
+
}
|
|
368
|
+
async updateSubscription(subscriptionGatewayId, input) {
|
|
369
|
+
if (input.idempotencyKey !== undefined) {
|
|
370
|
+
throw new Error('[payments] Square has no idempotency key on a subscription update: neither `PUT /v2/subscriptions/{id}` nor `POST /v2/subscriptions/{id}/swap-plan` takes an `idempotency_key`, unlike the create call. Accepting one here would turn your retry guarantee into a second plan swap, so it is refused rather than dropped.');
|
|
371
|
+
}
|
|
372
|
+
if (input.amount !== undefined) {
|
|
373
|
+
throw new Error('[payments] Square will not reprice a live subscription: `price_override_money` is set at creation and `UpdateSubscription` documents only `card_id` and `canceled_date` as changeable. Swap to a plan variation at the new price with `metadata: { planVariationId: "…" }`, which is a change Square actually applies.');
|
|
374
|
+
}
|
|
375
|
+
if (input.description !== undefined) {
|
|
376
|
+
throw new Error('[payments] A Square subscription has no description field. Keep it on your own record.');
|
|
377
|
+
}
|
|
378
|
+
const planVariationId = input.metadata?.planVariationId;
|
|
379
|
+
const cardId = input.metadata?.cardId;
|
|
380
|
+
if (typeof planVariationId !== 'string' && typeof cardId !== 'string') {
|
|
381
|
+
throw new Error('[payments] Nothing to update on a Square subscription. It accepts a plan swap (`metadata: { planVariationId }`) or a new card on file (`metadata: { cardId }`); anything else would be a change the gateway never sees.');
|
|
382
|
+
}
|
|
383
|
+
if (typeof planVariationId === 'string') {
|
|
384
|
+
const swapped = await this.#request(`/subscriptions/${subscriptionGatewayId}/swap-plan`, { method: 'POST', body: { new_plan_variation_id: planVariationId } });
|
|
385
|
+
return this.#mapSubscription(swapped.subscription);
|
|
386
|
+
}
|
|
387
|
+
const data = await this.#request(`/subscriptions/${subscriptionGatewayId}`, { method: 'PUT', body: { subscription: { card_id: cardId } } });
|
|
388
|
+
return this.#mapSubscription(data.subscription);
|
|
389
|
+
}
|
|
390
|
+
async findSubscription(gatewayId) {
|
|
391
|
+
try {
|
|
392
|
+
const data = await this.#request(`/subscriptions/${gatewayId}`);
|
|
393
|
+
return this.#mapSubscription(data.subscription);
|
|
394
|
+
}
|
|
395
|
+
catch (error) {
|
|
396
|
+
if (isNotFound(error))
|
|
397
|
+
return null;
|
|
398
|
+
throw error;
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
// ── Invoices ─────────────────────────────────────────────────────────────────────────
|
|
402
|
+
async listInvoices(customerId) {
|
|
403
|
+
// `GET /v2/invoices` filters by location only; the search endpoint is the one that
|
|
404
|
+
// takes a customer. Square documents the filter as accepting one location and one
|
|
405
|
+
// customer, which is exactly this call.
|
|
406
|
+
const data = await this.#request('/invoices/search', {
|
|
407
|
+
method: 'POST',
|
|
408
|
+
body: {
|
|
409
|
+
query: { filter: { location_ids: [this.#locationId], customer_ids: [customerId] } },
|
|
410
|
+
limit: 100,
|
|
411
|
+
},
|
|
412
|
+
});
|
|
413
|
+
return (data.invoices ?? []).map((invoice) => this.#mapInvoice(invoice));
|
|
414
|
+
}
|
|
415
|
+
// ── Webhooks ─────────────────────────────────────────────────────────────────────────
|
|
416
|
+
/**
|
|
417
|
+
* Whether a delivery to `POST /payments/webhook/:provider` can be authenticated.
|
|
418
|
+
*
|
|
419
|
+
* Square signs `notificationUrl + body`, so BOTH are needed — a signature key with no URL
|
|
420
|
+
* verifies nothing, which is why the check below tests for both.
|
|
421
|
+
*/
|
|
422
|
+
get webhookVerification() {
|
|
423
|
+
return this.#webhookSignatureKey !== undefined && this.#notificationUrl !== undefined
|
|
424
|
+
? 'configured'
|
|
425
|
+
: 'unconfigured';
|
|
426
|
+
}
|
|
427
|
+
parseWebhook(rawBody, headers) {
|
|
428
|
+
if (this.#webhookSignatureKey !== undefined && this.#notificationUrl !== undefined) {
|
|
429
|
+
const signature = headerValue(headers, 'x-square-hmacsha256-signature');
|
|
430
|
+
if (signature === undefined || signature === '') {
|
|
431
|
+
throw new Error('[payments] Missing x-square-hmacsha256-signature on Square webhook request.');
|
|
432
|
+
}
|
|
433
|
+
// The notification URL is prepended to the raw body BEFORE hashing — Square signs
|
|
434
|
+
// `url + body`, so a signature lifted from another endpoint of the same account does
|
|
435
|
+
// not verify here.
|
|
436
|
+
const signed = `${this.#notificationUrl}${rawBody}`;
|
|
437
|
+
if (!verifyHmacOverPayload(signed, signature, this.#webhookSignatureKey, 'sha256', 'base64')) {
|
|
438
|
+
throw new Error('[payments] Invalid Square webhook signature.');
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
const payload = JSON.parse(rawBody);
|
|
442
|
+
return {
|
|
443
|
+
id: payload.event_id ?? `${payload.type}-${payload.data?.id ?? ''}`,
|
|
444
|
+
provider: this.provider,
|
|
445
|
+
type: this.#mapWebhookType(payload),
|
|
446
|
+
createdAt: payload.created_at ?? new Date().toISOString(),
|
|
447
|
+
data: this.#mapWebhookData(payload),
|
|
448
|
+
raw: payload,
|
|
449
|
+
};
|
|
450
|
+
}
|
|
451
|
+
// ── Mappers ──────────────────────────────────────────────────────────────────────────
|
|
452
|
+
#mapCustomer(data) {
|
|
453
|
+
const name = [data.given_name, data.family_name].filter(Boolean).join(' ');
|
|
454
|
+
return {
|
|
455
|
+
id: data.id,
|
|
456
|
+
...(name !== '' ? { name } : {}),
|
|
457
|
+
...(data.email_address ? { email: data.email_address } : {}),
|
|
458
|
+
...(data.tax_ids?.eu_vat ? { taxId: data.tax_ids.eu_vat } : {}),
|
|
459
|
+
};
|
|
460
|
+
}
|
|
461
|
+
#mapPayment(data) {
|
|
462
|
+
const result = {
|
|
463
|
+
id: data.id,
|
|
464
|
+
gatewayId: data.id,
|
|
465
|
+
provider: this.provider,
|
|
466
|
+
amount: {
|
|
467
|
+
amount: data.amount_money?.amount ?? 0,
|
|
468
|
+
currency: (data.amount_money?.currency ?? this.#currency).toLowerCase(),
|
|
469
|
+
},
|
|
470
|
+
status: this.#mapPaymentStatus(data),
|
|
471
|
+
payload: data,
|
|
472
|
+
createdAt: data.created_at ?? new Date().toISOString(),
|
|
473
|
+
};
|
|
474
|
+
const method = this.#mapMethodToType(data);
|
|
475
|
+
if (method !== undefined)
|
|
476
|
+
result.method = method;
|
|
477
|
+
if (data.customer_id)
|
|
478
|
+
result.customerId = data.customer_id;
|
|
479
|
+
const paidAt = data.updated_at ?? data.created_at;
|
|
480
|
+
if (data.status === 'COMPLETED' && paidAt !== undefined)
|
|
481
|
+
result.paidAt = paidAt;
|
|
482
|
+
if (data.receipt_url)
|
|
483
|
+
result.hostedUrl = data.receipt_url;
|
|
484
|
+
return result;
|
|
485
|
+
}
|
|
486
|
+
/**
|
|
487
|
+
* `APPROVED` means Square is holding the funds on the buyer's card and nothing has moved
|
|
488
|
+
* to the seller; the authorization expires on its own if `CompletePayment` never runs.
|
|
489
|
+
* That is exactly `authorized` — it used to collapse into `pending`, which reads like a
|
|
490
|
+
* payment nobody has attempted rather than one whose money is reserved and ticking.
|
|
491
|
+
* `PENDING` stays `pending`: Square has not approved anything yet.
|
|
492
|
+
*
|
|
493
|
+
* A refunded payment stays `COMPLETED` at Square with a non-zero `refunded_money`, so
|
|
494
|
+
* that is read separately — otherwise a full refund would keep reporting as `paid`.
|
|
495
|
+
*/
|
|
496
|
+
#mapPaymentStatus(data) {
|
|
497
|
+
const refunded = data.refunded_money?.amount ?? 0;
|
|
498
|
+
const total = data.amount_money?.amount ?? 0;
|
|
499
|
+
if (refunded > 0 && refunded >= total)
|
|
500
|
+
return 'refunded';
|
|
501
|
+
switch (data.status) {
|
|
502
|
+
case 'COMPLETED':
|
|
503
|
+
return 'paid';
|
|
504
|
+
case 'APPROVED':
|
|
505
|
+
return 'authorized';
|
|
506
|
+
case 'FAILED':
|
|
507
|
+
return 'failed';
|
|
508
|
+
case 'CANCELED':
|
|
509
|
+
return 'canceled';
|
|
510
|
+
default:
|
|
511
|
+
return 'pending';
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
/**
|
|
515
|
+
* Square's `source_type` → the canonical {@link PaymentMethodType}.
|
|
516
|
+
*
|
|
517
|
+
* All of these now have a name: a Cash App payment is a `wallet`, an ACH one is a
|
|
518
|
+
* `bank_debit` (pulled from the buyer's account), Afterpay is `bnpl`. They used to come
|
|
519
|
+
* back unset, which is indistinguishable from "Square did not say" — and the Square
|
|
520
|
+
* balance (`SQUARE_ACCOUNT`) is a stored balance, which is what `wallet` means.
|
|
521
|
+
*
|
|
522
|
+
* `CASH` and `EXTERNAL` stay unset on purpose: they are money recorded as taken outside
|
|
523
|
+
* Square altogether, and no member of the union describes that.
|
|
524
|
+
*/
|
|
525
|
+
#mapMethodToType(data) {
|
|
526
|
+
switch (data.source_type) {
|
|
527
|
+
case 'CARD':
|
|
528
|
+
return data.card_details?.card?.card_type === 'DEBIT' ? 'debit_card' : 'card';
|
|
529
|
+
case 'WALLET':
|
|
530
|
+
case 'SQUARE_ACCOUNT':
|
|
531
|
+
return 'wallet';
|
|
532
|
+
case 'BANK_ACCOUNT':
|
|
533
|
+
return 'bank_debit';
|
|
534
|
+
case 'BUY_NOW_PAY_LATER':
|
|
535
|
+
return 'bnpl';
|
|
536
|
+
default:
|
|
537
|
+
return undefined;
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
#mapSubscription(data, customerId) {
|
|
541
|
+
const statusMap = {
|
|
542
|
+
PENDING: 'incomplete',
|
|
543
|
+
ACTIVE: 'active',
|
|
544
|
+
// A paused Square subscription bills nothing and will bill again when it resumes.
|
|
545
|
+
// `past_due` said the buyer owed money they did not; the subscriber is entitled to
|
|
546
|
+
// nothing either way, which is the part that must not change.
|
|
547
|
+
PAUSED: 'paused',
|
|
548
|
+
CANCELED: 'canceled',
|
|
549
|
+
DEACTIVATED: 'canceled',
|
|
550
|
+
COMPLETED: 'ended',
|
|
551
|
+
};
|
|
552
|
+
const result = {
|
|
553
|
+
id: data.id,
|
|
554
|
+
gatewayId: data.id,
|
|
555
|
+
provider: this.provider,
|
|
556
|
+
customerId: data.customer_id ?? customerId ?? '',
|
|
557
|
+
status: (data.status !== undefined ? statusMap[data.status] : undefined) ?? 'active',
|
|
558
|
+
planId: data.plan_variation_id ?? '',
|
|
559
|
+
payload: data,
|
|
560
|
+
createdAt: data.created_at ?? new Date().toISOString(),
|
|
561
|
+
};
|
|
562
|
+
if (data.price_override_money?.amount !== undefined) {
|
|
563
|
+
result.amount = {
|
|
564
|
+
amount: data.price_override_money.amount,
|
|
565
|
+
currency: (data.price_override_money.currency ?? this.#currency).toLowerCase(),
|
|
566
|
+
};
|
|
567
|
+
}
|
|
568
|
+
// Square's dates are `YYYY-MM-DD`, not timestamps; they are widened to UTC midnight so
|
|
569
|
+
// the whole package keeps holding ISO instants.
|
|
570
|
+
const startsAt = this.#toIso(data.start_date);
|
|
571
|
+
const endsAt = this.#toIso(data.canceled_date);
|
|
572
|
+
const chargedThrough = this.#toIso(data.charged_through_date);
|
|
573
|
+
if (startsAt !== undefined)
|
|
574
|
+
result.currentPeriodStart = startsAt;
|
|
575
|
+
if (chargedThrough !== undefined)
|
|
576
|
+
result.currentPeriodEnd = chargedThrough;
|
|
577
|
+
if (endsAt !== undefined)
|
|
578
|
+
result.endsAt = endsAt;
|
|
579
|
+
return result;
|
|
580
|
+
}
|
|
581
|
+
#mapInvoice(data) {
|
|
582
|
+
const statusMap = {
|
|
583
|
+
DRAFT: 'draft',
|
|
584
|
+
UNPAID: 'open',
|
|
585
|
+
SCHEDULED: 'open',
|
|
586
|
+
PARTIALLY_PAID: 'open',
|
|
587
|
+
PAID: 'paid',
|
|
588
|
+
PARTIALLY_REFUNDED: 'paid',
|
|
589
|
+
REFUNDED: 'paid',
|
|
590
|
+
CANCELED: 'canceled',
|
|
591
|
+
FAILED: 'failed',
|
|
592
|
+
PAYMENT_PENDING: 'pending',
|
|
593
|
+
};
|
|
594
|
+
const amount = data.payment_requests?.[0]?.computed_amount_money?.amount ??
|
|
595
|
+
data.next_payment_amount_money?.amount ??
|
|
596
|
+
0;
|
|
597
|
+
const currency = data.payment_requests?.[0]?.computed_amount_money?.currency ??
|
|
598
|
+
data.next_payment_amount_money?.currency ??
|
|
599
|
+
this.#currency;
|
|
600
|
+
return {
|
|
601
|
+
id: data.id,
|
|
602
|
+
gatewayId: data.id,
|
|
603
|
+
provider: this.provider,
|
|
604
|
+
...(data.primary_recipient?.customer_id
|
|
605
|
+
? { customerId: data.primary_recipient.customer_id }
|
|
606
|
+
: {}),
|
|
607
|
+
...(data.subscription_id ? { subscriptionId: data.subscription_id } : {}),
|
|
608
|
+
status: (data.status !== undefined ? statusMap[data.status] : undefined) ?? 'draft',
|
|
609
|
+
amount: { amount, currency: currency.toLowerCase() },
|
|
610
|
+
createdAt: data.created_at ?? new Date().toISOString(),
|
|
611
|
+
// `public_url` is Square's hosted invoice page, not a PDF; it is the closest thing
|
|
612
|
+
// the API offers and the field a reader will go looking in.
|
|
613
|
+
...(data.public_url ? { hostedPdfUrl: data.public_url } : {}),
|
|
614
|
+
...(data.invoice_number ? { number: data.invoice_number } : {}),
|
|
615
|
+
payload: data,
|
|
616
|
+
};
|
|
617
|
+
}
|
|
618
|
+
#mapWebhookType(payload) {
|
|
619
|
+
const object = payload.data?.object;
|
|
620
|
+
switch (payload.type) {
|
|
621
|
+
case 'payment.created':
|
|
622
|
+
case 'payment.updated': {
|
|
623
|
+
const payment = object?.payment;
|
|
624
|
+
if (payment === undefined)
|
|
625
|
+
return 'payment.updated';
|
|
626
|
+
const status = this.#mapPaymentStatus(payment);
|
|
627
|
+
if (status === 'paid')
|
|
628
|
+
return 'payment.succeeded';
|
|
629
|
+
if (status === 'refunded')
|
|
630
|
+
return 'payment.refunded';
|
|
631
|
+
if (status === 'failed' || status === 'canceled')
|
|
632
|
+
return 'payment.failed';
|
|
633
|
+
return 'payment.updated';
|
|
634
|
+
}
|
|
635
|
+
case 'refund.created':
|
|
636
|
+
case 'refund.updated':
|
|
637
|
+
// Only a COMPLETED refund means money left; `PENDING` and `REJECTED` are not a
|
|
638
|
+
// refund the ledger should record as one.
|
|
639
|
+
return object?.refund?.status === 'COMPLETED' ? 'payment.refunded' : 'payment.updated';
|
|
640
|
+
case 'invoice.payment_made':
|
|
641
|
+
return 'payment.succeeded';
|
|
642
|
+
// ── The dispute family ───────────────────────────────────────────────────────────
|
|
643
|
+
// Square answers the money question outright for a chargeback: when the bank notifies
|
|
644
|
+
// it of one, "Square withholds the disputed funds from the seller's Square account
|
|
645
|
+
// balance until the bank issues a final resolution on the case. If there are
|
|
646
|
+
// insufficient funds in the Square account balance, the funds are removed (debited)
|
|
647
|
+
// from the seller's most recently linked bank account." So `dispute.created` is
|
|
648
|
+
// `payment.disputed` — unless the state says this is an inquiry, see
|
|
649
|
+
// {@link SquareDriver.#disputeType}. `dispute.state.updated` carries the whole Dispute
|
|
650
|
+
// object, so it is read the same way and is where the resolution arrives.
|
|
651
|
+
case 'dispute.created':
|
|
652
|
+
case 'dispute.state.updated':
|
|
653
|
+
// The deprecated spelling of `dispute.state.updated`; an app still subscribed to it
|
|
654
|
+
// should not silently miss the resolution.
|
|
655
|
+
case 'dispute.state.changed':
|
|
656
|
+
return this.#disputeType(object?.dispute, payload.type);
|
|
657
|
+
// Evidence going in and out is paperwork inside an open dispute, not a resolution of
|
|
658
|
+
// it. `.created`/`.deleted` are the current names, `.added`/`.removed` the deprecated
|
|
659
|
+
// ones Square still publishes.
|
|
660
|
+
case 'dispute.evidence.added':
|
|
661
|
+
case 'dispute.evidence.created':
|
|
662
|
+
case 'dispute.evidence.deleted':
|
|
663
|
+
case 'dispute.evidence.removed':
|
|
664
|
+
return 'payment.updated';
|
|
665
|
+
case 'subscription.created':
|
|
666
|
+
return 'subscription.created';
|
|
667
|
+
case 'subscription.updated': {
|
|
668
|
+
const status = object?.subscription?.status;
|
|
669
|
+
return status === 'CANCELED' || status === 'DEACTIVATED'
|
|
670
|
+
? 'subscription.canceled'
|
|
671
|
+
: 'subscription.updated';
|
|
672
|
+
}
|
|
673
|
+
default:
|
|
674
|
+
return payload.type;
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
/**
|
|
678
|
+
* A Dispute's `state` → the canonical event type.
|
|
679
|
+
*
|
|
680
|
+
* Square's `DisputeState` enum has eight values and splits cleanly in two. Four of them
|
|
681
|
+
* are a real chargeback — `EVIDENCE_REQUIRED` ("the initial state of a dispute with
|
|
682
|
+
* evidence required"), `PROCESSING` ("dispute evidence has been submitted and the bank is
|
|
683
|
+
* processing the dispute"), `WON` and `LOST` — and three more are prefixed `INQUIRY_`,
|
|
684
|
+
* which Square's own enum descriptions call "an inquiry" rather than a dispute.
|
|
685
|
+
*
|
|
686
|
+
* The inquiry states become `payment.dispute_warning`. **Square's reference does not say
|
|
687
|
+
* in words whether an inquiry withholds funds**: the "Square withholds the disputed funds"
|
|
688
|
+
* sentence is written about a cardholder requesting a charge reversal, and the support
|
|
689
|
+
* article on information requests says nothing about money at all. What the reference does
|
|
690
|
+
* do is name these states inquiries and keep them out of the dispute states, which is the
|
|
691
|
+
* same distinction Stripe draws with its `warning_*` statuses and the networks draw
|
|
692
|
+
* between a retrieval request and a chargeback. Reporting an inquiry as a warning writes
|
|
693
|
+
* nothing to the ledger; reporting it as `payment.disputed` moves a paid row.
|
|
694
|
+
*
|
|
695
|
+
* `INQUIRY_CLOSED` is "the inquiry is complete" and names no winner, so it stays a
|
|
696
|
+
* `payment.updated` — the processor throws on a close with no outcome precisely so a
|
|
697
|
+
* driver that cannot read one emits an update instead.
|
|
698
|
+
*
|
|
699
|
+
* `ACCEPTED` is a **loss**: `AcceptDispute` is documented as "Square returns the disputed
|
|
700
|
+
* amount to the cardholder and updates the dispute state to `ACCEPTED`. The dispute is now
|
|
701
|
+
* closed." The seller accepted liability and the money is gone, exactly as with Adyen's
|
|
702
|
+
* `Accepted` disputeStatus.
|
|
703
|
+
*
|
|
704
|
+
* With no state at all — Square marks the field nullable — `dispute.created` keeps the
|
|
705
|
+
* `payment.disputed` it has always had, and a state change with nothing to change stays an
|
|
706
|
+
* update.
|
|
707
|
+
*/
|
|
708
|
+
#disputeType(dispute, eventType) {
|
|
709
|
+
switch (dispute?.state) {
|
|
710
|
+
case 'INQUIRY_EVIDENCE_REQUIRED':
|
|
711
|
+
case 'INQUIRY_PROCESSING':
|
|
712
|
+
return 'payment.dispute_warning';
|
|
713
|
+
case 'EVIDENCE_REQUIRED':
|
|
714
|
+
case 'PROCESSING':
|
|
715
|
+
return 'payment.disputed';
|
|
716
|
+
case 'WON':
|
|
717
|
+
case 'LOST':
|
|
718
|
+
case 'ACCEPTED':
|
|
719
|
+
return 'payment.dispute_closed';
|
|
720
|
+
default:
|
|
721
|
+
return eventType === 'dispute.created' ? 'payment.disputed' : 'payment.updated';
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
/** The three terminal states, and only those. `INQUIRY_CLOSED` names no winner. */
|
|
725
|
+
#disputeOutcome(state) {
|
|
726
|
+
if (state === 'WON')
|
|
727
|
+
return 'won';
|
|
728
|
+
if (state === 'LOST' || state === 'ACCEPTED')
|
|
729
|
+
return 'lost';
|
|
730
|
+
return undefined;
|
|
731
|
+
}
|
|
732
|
+
#mapWebhookData(payload) {
|
|
733
|
+
const object = payload.data?.object;
|
|
734
|
+
const subscription = object?.subscription;
|
|
735
|
+
if (subscription !== undefined) {
|
|
736
|
+
const mapped = this.#mapSubscription(subscription);
|
|
737
|
+
return {
|
|
738
|
+
gatewayId: mapped.gatewayId,
|
|
739
|
+
customerId: mapped.customerId,
|
|
740
|
+
status: mapped.status,
|
|
741
|
+
planId: mapped.planId,
|
|
742
|
+
...(mapped.endsAt !== undefined ? { endsAt: mapped.endsAt } : {}),
|
|
743
|
+
};
|
|
744
|
+
}
|
|
745
|
+
const payment = object?.payment;
|
|
746
|
+
if (payment !== undefined) {
|
|
747
|
+
const mapped = this.#mapPayment(payment);
|
|
748
|
+
const externalReference = this.#externalReferenceOf(payment);
|
|
749
|
+
return {
|
|
750
|
+
gatewayId: mapped.gatewayId,
|
|
751
|
+
amount: mapped.amount.amount,
|
|
752
|
+
currency: mapped.amount.currency,
|
|
753
|
+
status: mapped.status,
|
|
754
|
+
...(mapped.customerId !== undefined ? { customerId: mapped.customerId } : {}),
|
|
755
|
+
...(payment.order_id ? { orderId: payment.order_id } : {}),
|
|
756
|
+
...(externalReference !== undefined ? { externalReference } : {}),
|
|
757
|
+
};
|
|
758
|
+
}
|
|
759
|
+
const dispute = object?.dispute;
|
|
760
|
+
if (dispute !== undefined) {
|
|
761
|
+
// Keyed on the DISPUTED PAYMENT, not on the dispute: the ledger row that has to stop
|
|
762
|
+
// saying `paid` is the payment's. `amount_money` is the disputed amount, which for a
|
|
763
|
+
// partial dispute is less than the payment — `event.raw` carries the reason, the
|
|
764
|
+
// state and the evidence deadline.
|
|
765
|
+
const outcome = this.#disputeOutcome(dispute.state);
|
|
766
|
+
return {
|
|
767
|
+
gatewayId: dispute.disputed_payment?.payment_id ?? '',
|
|
768
|
+
amount: dispute.amount_money?.amount ?? 0,
|
|
769
|
+
currency: (dispute.amount_money?.currency ?? this.#currency).toLowerCase(),
|
|
770
|
+
...(dispute.id !== undefined ? { disputeId: dispute.id } : {}),
|
|
771
|
+
...(dispute.reason !== undefined ? { reason: dispute.reason } : {}),
|
|
772
|
+
// "The deadline by which the seller must respond to the dispute", already RFC 3339
|
|
773
|
+
// — and if it passes with no action Square automatically challenges on the seller's
|
|
774
|
+
// behalf, which is not the same as the seller having decided anything.
|
|
775
|
+
...(dispute.due_at !== undefined ? { actionableUntil: dispute.due_at } : {}),
|
|
776
|
+
...(dispute.state !== undefined ? { disputeState: dispute.state } : {}),
|
|
777
|
+
...(outcome !== undefined ? { outcome } : {}),
|
|
778
|
+
};
|
|
779
|
+
}
|
|
780
|
+
const refund = object?.refund;
|
|
781
|
+
if (refund !== undefined) {
|
|
782
|
+
return {
|
|
783
|
+
// The ledger keys refunds on the PAYMENT, which is the row the amount came off.
|
|
784
|
+
gatewayId: refund.payment_id ?? refund.id,
|
|
785
|
+
amount: refund.amount_money?.amount ?? 0,
|
|
786
|
+
currency: (refund.amount_money?.currency ?? this.#currency).toLowerCase(),
|
|
787
|
+
refundId: refund.id,
|
|
788
|
+
};
|
|
789
|
+
}
|
|
790
|
+
const invoice = object?.invoice;
|
|
791
|
+
if (invoice !== undefined) {
|
|
792
|
+
const mapped = this.#mapInvoice(invoice);
|
|
793
|
+
return {
|
|
794
|
+
gatewayId: mapped.gatewayId,
|
|
795
|
+
amount: mapped.amount.amount,
|
|
796
|
+
currency: mapped.amount.currency,
|
|
797
|
+
...(mapped.customerId !== undefined ? { customerId: mapped.customerId } : {}),
|
|
798
|
+
...(mapped.subscriptionId !== undefined ? { subscriptionId: mapped.subscriptionId } : {}),
|
|
799
|
+
};
|
|
800
|
+
}
|
|
801
|
+
const order = object?.order;
|
|
802
|
+
if (order?.id !== undefined) {
|
|
803
|
+
return {
|
|
804
|
+
gatewayId: order.id,
|
|
805
|
+
amount: order.total_money?.amount ?? 0,
|
|
806
|
+
currency: (order.total_money?.currency ?? this.#currency).toLowerCase(),
|
|
807
|
+
...(order.customer_id ? { customerId: order.customer_id } : {}),
|
|
808
|
+
...(this.#orderReference(order) !== undefined
|
|
809
|
+
? { externalReference: this.#orderReference(order) }
|
|
810
|
+
: {}),
|
|
811
|
+
};
|
|
812
|
+
}
|
|
813
|
+
return {};
|
|
814
|
+
}
|
|
815
|
+
// ── Helpers ──────────────────────────────────────────────────────────────────────────
|
|
816
|
+
/**
|
|
817
|
+
* Where a Square payment carries the app's own reference.
|
|
818
|
+
*
|
|
819
|
+
* `reference_id` is what {@link SquareDriver.charge} sets, and it comes back on the
|
|
820
|
+
* payment. A payment made through a hosted link is the awkward case: the reference lives
|
|
821
|
+
* on the ORDER, and Square's published `payment.created`/`payment.updated` examples do
|
|
822
|
+
* not show `reference_id` on the payment — which is why `createCheckout` also sends
|
|
823
|
+
* `payment_note`, read here as a fallback.
|
|
824
|
+
*/
|
|
825
|
+
#externalReferenceOf(payment) {
|
|
826
|
+
return payment.reference_id ?? payment.note ?? undefined;
|
|
827
|
+
}
|
|
828
|
+
#orderReference(order) {
|
|
829
|
+
return order.reference_id ?? order.metadata?.external_reference ?? undefined;
|
|
830
|
+
}
|
|
831
|
+
/** Order metadata keys are `[a-zA-Z0-9_-]`, values 255 chars, ten entries at most. */
|
|
832
|
+
#orderMetadata(externalReference, extra) {
|
|
833
|
+
const metadata = {};
|
|
834
|
+
for (const [key, value] of Object.entries(extra ?? {})) {
|
|
835
|
+
if (value === undefined || value === null)
|
|
836
|
+
continue;
|
|
837
|
+
metadata[key.replace(/[^a-zA-Z0-9_-]/g, '_').slice(0, 60)] = String(value).slice(0, 255);
|
|
838
|
+
}
|
|
839
|
+
if (externalReference !== undefined)
|
|
840
|
+
metadata.external_reference = externalReference;
|
|
841
|
+
return metadata;
|
|
842
|
+
}
|
|
843
|
+
#reference(externalReference, where) {
|
|
844
|
+
if (externalReference === undefined)
|
|
845
|
+
return undefined;
|
|
846
|
+
if (externalReference.length > REFERENCE_MAX_LENGTH) {
|
|
847
|
+
throw new Error(`[payments] Square carries \`externalReference\` as \`reference_id\`, capped at ${REFERENCE_MAX_LENGTH} characters; the ${where} got ${externalReference.length}.`);
|
|
848
|
+
}
|
|
849
|
+
return externalReference;
|
|
850
|
+
}
|
|
851
|
+
#idempotencyKey(key) {
|
|
852
|
+
if (key === undefined)
|
|
853
|
+
return randomUUID();
|
|
854
|
+
if (key.length > IDEMPOTENCY_MAX_LENGTH) {
|
|
855
|
+
throw new Error(`[payments] Square caps \`idempotency_key\` at ${IDEMPOTENCY_MAX_LENGTH} characters; got ${key.length}.`);
|
|
856
|
+
}
|
|
857
|
+
return key;
|
|
858
|
+
}
|
|
859
|
+
/** Square's tax id field is EU VAT and nothing else, so an arbitrary tax id is refused. */
|
|
860
|
+
#taxIds(taxId, metadata) {
|
|
861
|
+
if (taxId === undefined)
|
|
862
|
+
return {};
|
|
863
|
+
if (metadata?.taxIdType !== 'eu_vat') {
|
|
864
|
+
throw new Error('[payments] A Square customer has exactly one tax field, `tax_ids.eu_vat`, and no general-purpose one. Pass `metadata: { taxIdType: "eu_vat" }` if that is what `taxId` is; otherwise keep the tax id on your own record rather than having it silently dropped here.');
|
|
865
|
+
}
|
|
866
|
+
return { tax_ids: { eu_vat: taxId } };
|
|
867
|
+
}
|
|
868
|
+
/** Square subscription dates are `YYYY-MM-DD`; the contract wants an ISO instant. */
|
|
869
|
+
#startDate(value) {
|
|
870
|
+
const date = new Date(value);
|
|
871
|
+
if (Number.isNaN(date.getTime())) {
|
|
872
|
+
throw new Error(`[payments] Square: \`startDate\` "${value}" is not a date.`);
|
|
873
|
+
}
|
|
874
|
+
return date.toISOString().slice(0, 10);
|
|
875
|
+
}
|
|
876
|
+
#toIso(value) {
|
|
877
|
+
if (value === undefined || value === '')
|
|
878
|
+
return undefined;
|
|
879
|
+
const date = new Date(`${value}T00:00:00.000Z`);
|
|
880
|
+
if (Number.isNaN(date.getTime()))
|
|
881
|
+
return undefined;
|
|
882
|
+
return date.toISOString();
|
|
883
|
+
}
|
|
884
|
+
async #paymentAmount(paymentGatewayId) {
|
|
885
|
+
const data = await this.#request(`/payments/${paymentGatewayId}`);
|
|
886
|
+
const amount = data.payment.amount_money?.amount;
|
|
887
|
+
if (amount === undefined) {
|
|
888
|
+
throw new Error(`[payments] Square payment "${paymentGatewayId}" has no amount to refund. \`RefundPayment\` requires \`amount_money\`, so pass the amount explicitly.`);
|
|
889
|
+
}
|
|
890
|
+
return {
|
|
891
|
+
amount,
|
|
892
|
+
currency: (data.payment.amount_money?.currency ?? this.#currency).toUpperCase(),
|
|
893
|
+
};
|
|
894
|
+
}
|
|
895
|
+
async #request(path, options = {}) {
|
|
896
|
+
return httpRequest(path, {
|
|
897
|
+
baseUrl: this.#baseUrl,
|
|
898
|
+
...(options.method !== undefined ? { method: options.method } : {}),
|
|
899
|
+
...(options.body !== undefined ? { body: options.body } : {}),
|
|
900
|
+
bearerToken: this.#accessToken,
|
|
901
|
+
headers: { 'Square-Version': this.#apiVersion },
|
|
902
|
+
});
|
|
903
|
+
}
|
|
904
|
+
}
|
|
905
|
+
//# sourceMappingURL=square.js.map
|