awesome-node-checkout 1.3.0 → 2.1.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.
Files changed (59) hide show
  1. package/CHANGELOG.md +64 -0
  2. package/README.md +335 -17
  3. package/dist/adapters/express/index.d.ts +103 -2
  4. package/dist/adapters/express/index.d.ts.map +1 -1
  5. package/dist/adapters/express/index.js +213 -30
  6. package/dist/adapters/express/index.js.map +1 -1
  7. package/dist/checkout-configurator.d.ts +106 -0
  8. package/dist/checkout-configurator.d.ts.map +1 -1
  9. package/dist/checkout-configurator.js +515 -0
  10. package/dist/checkout-configurator.js.map +1 -1
  11. package/dist/events/checkout-event-bus.d.ts +11 -1
  12. package/dist/events/checkout-event-bus.d.ts.map +1 -1
  13. package/dist/events/checkout-event-bus.js.map +1 -1
  14. package/dist/index.d.ts +5 -0
  15. package/dist/index.d.ts.map +1 -1
  16. package/dist/index.js +8 -1
  17. package/dist/index.js.map +1 -1
  18. package/dist/interfaces/mandate-provider.interface.d.ts +40 -0
  19. package/dist/interfaces/mandate-provider.interface.d.ts.map +1 -0
  20. package/dist/interfaces/mandate-provider.interface.js +18 -0
  21. package/dist/interfaces/mandate-provider.interface.js.map +1 -0
  22. package/dist/models/checkout-config.model.d.ts +16 -0
  23. package/dist/models/checkout-config.model.d.ts.map +1 -1
  24. package/dist/models/errors.d.ts +1 -1
  25. package/dist/models/errors.d.ts.map +1 -1
  26. package/dist/models/errors.js.map +1 -1
  27. package/dist/models/mandate.model.d.ts +166 -0
  28. package/dist/models/mandate.model.d.ts.map +1 -0
  29. package/dist/models/mandate.model.js +19 -0
  30. package/dist/models/mandate.model.js.map +1 -0
  31. package/dist/providers/nexi/nexi.provider.d.ts +125 -1
  32. package/dist/providers/nexi/nexi.provider.d.ts.map +1 -1
  33. package/dist/providers/nexi/nexi.provider.js +677 -13
  34. package/dist/providers/nexi/nexi.provider.js.map +1 -1
  35. package/dist/providers/paypal/paypal.provider.d.ts +44 -1
  36. package/dist/providers/paypal/paypal.provider.d.ts.map +1 -1
  37. package/dist/providers/paypal/paypal.provider.js +529 -2
  38. package/dist/providers/paypal/paypal.provider.js.map +1 -1
  39. package/dist/providers/satispay/satispay.provider.d.ts +86 -1
  40. package/dist/providers/satispay/satispay.provider.d.ts.map +1 -1
  41. package/dist/providers/satispay/satispay.provider.js +440 -3
  42. package/dist/providers/satispay/satispay.provider.js.map +1 -1
  43. package/dist/utils/ids.d.ts +10 -0
  44. package/dist/utils/ids.d.ts.map +1 -0
  45. package/dist/utils/ids.js +16 -0
  46. package/dist/utils/ids.js.map +1 -0
  47. package/dist/utils/money.d.ts +20 -0
  48. package/dist/utils/money.d.ts.map +1 -0
  49. package/dist/utils/money.js +68 -0
  50. package/dist/utils/money.js.map +1 -0
  51. package/dist/utils/redact.d.ts +15 -0
  52. package/dist/utils/redact.d.ts.map +1 -0
  53. package/dist/utils/redact.js +60 -0
  54. package/dist/utils/redact.js.map +1 -0
  55. package/dist/utils/url.d.ts +12 -0
  56. package/dist/utils/url.d.ts.map +1 -0
  57. package/dist/utils/url.js +19 -0
  58. package/dist/utils/url.js.map +1 -0
  59. package/package.json +2 -2
@@ -2,6 +2,8 @@ import { BasePaymentProvider } from '../../abstract/base-payment-provider.abstra
2
2
  import { PaymentRequest } from '../../models/payment-request.model';
3
3
  import { PaymentResult, WebhookResult } from '../../models/payment-result.model';
4
4
  import { ITransactionStore } from '../../interfaces/transaction-store.interface';
5
+ import { IMandateProvider } from '../../interfaces/mandate-provider.interface';
6
+ import { ChargeRequest, ChargeResult, Mandate, MandateCallbackInput, MandatePendingState, MandateRevokeResult, MandateSetupOutcome, MandateSetupRequest, MandateSetupStart, MandateStatusResult } from '../../models/mandate.model';
5
7
  export interface SatispayProviderConfig {
6
8
  keyId: string;
7
9
  /**
@@ -42,14 +44,30 @@ export interface SatispayProviderConfig {
42
44
  * Use a persistent store (Redis, DB) in multi-instance deployments.
43
45
  */
44
46
  transactionStore?: ITransactionStore;
47
+ /**
48
+ * Timeout of the mandate API calls (`createMandate`, `confirmMandate`, `chargeMandate`,
49
+ * `getMandate`) in milliseconds. A timeout is reported as `unknown`, never `failed`.
50
+ * @default 30000
51
+ */
52
+ timeoutMs?: number;
53
+ /**
54
+ * Path of the mandate callback route, appended to `serverUrl` to build the authorization's
55
+ * `callback_url` when `MandateSetupRequest.notifyUrl` is not set.
56
+ * @default '/checkout/satispay/mandate/callback'
57
+ */
58
+ mandateCallbackPath?: string;
45
59
  }
46
60
  /**
47
61
  * Satispay Business API provider.
48
62
  * Flow: `webhook` — creates a MATCH_CODE payment, user pays via Satispay app,
49
63
  * Satispay calls the `callback_url` webhook, which is correlated to the order
50
64
  * via the `ITransactionStore`. The redirect is then verified via `handleRedirect`.
65
+ *
66
+ * Recurring payments (`IMandateProvider`): Automatic Payments, web-redirect flow. A setup creates
67
+ * a pre-authorized payment token, the customer approves it in Satispay, and every charge (the
68
+ * first one included) is a synchronous `PRE_AUTHORIZED` payment. EUR only.
51
69
  */
52
- export declare class SatispayProvider extends BasePaymentProvider {
70
+ export declare class SatispayProvider extends BasePaymentProvider implements IMandateProvider {
53
71
  private readonly config;
54
72
  readonly name = "satispay";
55
73
  readonly flow: "webhook";
@@ -87,6 +105,13 @@ export declare class SatispayProvider extends BasePaymentProvider {
87
105
  rawBody?: Buffer | string;
88
106
  }): boolean;
89
107
  private request;
108
+ /**
109
+ * Signed request that never throws and keeps the HTTP status and the parsed body, for the
110
+ * mandate methods (which must tell 4xx from 5xx from a timeout). `extraHeaders` are merged after
111
+ * the signature headers but can never override `Host`, `Date`, `Digest` or `Authorization`
112
+ * (case-insensitively). `request()` keeps its throwing behaviour for the one-off payment flow.
113
+ */
114
+ private send;
90
115
  createPayment(request: PaymentRequest): Promise<PaymentResult>;
91
116
  /** For Satispay, execute is equivalent to a status check */
92
117
  executePayment(paymentId: string): Promise<PaymentResult>;
@@ -117,5 +142,65 @@ export declare class SatispayProvider extends BasePaymentProvider {
117
142
  * Looks up the transaction by `order_id` in the store, then verifies status.
118
143
  */
119
144
  handleRedirect(query: Record<string, string>): Promise<PaymentResult>;
145
+ /**
146
+ * Creates a pre-authorized payment token (`POST /pre_authorized_payment_tokens`) and returns
147
+ * its `redirect_url` as `approvalUrl`. Never open that URL in a WebView.
148
+ *
149
+ * - EUR only.
150
+ * - `callback_url` is `notifyUrl` (or `serverUrl + mandateCallbackPath`) with `mandate_ref`
151
+ * and the raw `token_id={uuid}` placeholder, which Satispay substitutes with the token id.
152
+ * - `redirect_url` is `returnUrl` with `mandate_ref`.
153
+ * - `cancelUrl` is unused: Satispay has no cancel redirect (a canceled authorization is
154
+ * reported through the callback and re-read as `CANCELED`).
155
+ * - `Idempotency-Key: setup-<orderId>`.
156
+ */
157
+ createMandate(request: MandateSetupRequest): Promise<MandateSetupStart>;
158
+ /**
159
+ * Re-reads the authorization server-to-server (the callback is unsigned) and, once it is
160
+ * `ACCEPTED`, makes the first `PRE_AUTHORIZED` payment with `Idempotency-Key = orderId`. A second
161
+ * confirm (a second callback, or a re-confirm job with `{}`) replays that key and gets Satispay's
162
+ * original response instead of charging twice. The token id is always `pending.providerRef`;
163
+ * a callback `token_id` only has to match it.
164
+ *
165
+ * `CANCELED` is terminal with no mandate. The first payment is replayed with the same body and
166
+ * `Idempotency-Key = orderId` (Satispay returns the original response for a reused key), so a
167
+ * re-confirm after an `unknown` first charge learns whether it was taken:
168
+ * - `ACCEPTED` (the authorization was canceled after the first charge): `success: false`,
169
+ * verified, terminal, `succeeded` first charge, error 'Authorization canceled after the first
170
+ * charge'. The app refunds it or keeps it as a one-off payment.
171
+ * - a 4xx (no payment exists for that key, e.g. the customer declined the authorization):
172
+ * `success: false`, verified, terminal, `failed` first charge ('Authorization canceled').
173
+ * - 401, 403, 408, 409, 429, 5xx, a network error or an unexpected 2xx: unverified, non-terminal,
174
+ * `unknown` first charge.
175
+ * The replay is only authoritative while Satispay keeps the key (retention not documented).
176
+ */
177
+ confirmMandate(pending: MandatePendingState, input: MandateCallbackInput): Promise<MandateSetupOutcome>;
178
+ /**
179
+ * Merchant-initiated `PRE_AUTHORIZED` payment, synchronous: Satispay creates it `ACCEPTED` or
180
+ * answers 4xx without creating a payment. `Idempotency-Key = chargeRef`: the same `chargeRef`
181
+ * gets Satispay's original response (retention not documented: retry `unknown` promptly).
182
+ * `providerPaymentId` is the Satispay payment id, refundable with `refundPayment`.
183
+ * 408 and 409 are `unknown`, not a refusal: they may answer a concurrent request with the same
184
+ * key that is still in flight (Satispay documents neither the behaviour nor the status).
185
+ */
186
+ chargeMandate(mandate: Mandate, charge: ChargeRequest): Promise<ChargeResult>;
187
+ /**
188
+ * `POST /payments` (`PRE_AUTHORIZED`, `Idempotency-Key = chargeRef`) mapped to a `ChargeResult`,
189
+ * without the token re-read that sets `mandateUnusable`. A verified `failed` is always a 4xx.
190
+ */
191
+ private postPayment;
192
+ /** Re-reads the authorization: ACCEPTED → active, CANCELED or 404 → revoked, PENDING → unknown. */
193
+ getMandate(mandate: Mandate): Promise<MandateStatusResult>;
194
+ /**
195
+ * Makes no network call: Satispay documents no merchant-side revocation (the token API has only
196
+ * GET). The undocumented `PUT /pre_authorized_payment_tokens/{id}` is deliberately not used.
197
+ * Stop charging the mandate; the customer can revoke it from the Satispay app.
198
+ */
199
+ revokeMandate(_mandate: Mandate): Promise<MandateRevokeResult>;
200
+ /**
201
+ * `GET /pre_authorized_payment_tokens/{id}`. A 2xx whose `id` is not `tokenId` or whose `status`
202
+ * is not a string, a network error, a 5xx and any other non-2xx except 404 are `unavailable`.
203
+ */
204
+ private readToken;
120
205
  }
121
206
  //# sourceMappingURL=satispay.provider.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"satispay.provider.d.ts","sourceRoot":"","sources":["../../../src/providers/satispay/satispay.provider.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,mBAAmB,EAAE,MAAM,+CAA+C,CAAC;AACpF,OAAO,EAAE,cAAc,EAAE,MAAM,oCAAoC,CAAC;AACpE,OAAO,EAAE,aAAa,EAAE,aAAa,EAAE,MAAM,mCAAmC,CAAC;AACjF,OAAO,EAAE,iBAAiB,EAAE,MAAM,8CAA8C,CAAC;AAIjF,MAAM,WAAW,sBAAsB;IACrC,KAAK,EAAE,MAAM,CAAC;IACd;;;OAGG;IACH,UAAU,EAAE,MAAM,CAAC;IACnB;;;;;;OAMG;IACH,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,yBAAyB;IACzB,WAAW,CAAC,EAAE,SAAS,GAAG,YAAY,CAAC;IACvC;;;;OAIG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;;;OAGG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB;;;;OAIG;IACH,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B;;;;OAIG;IACH,gBAAgB,CAAC,EAAE,iBAAiB,CAAC;CACtC;AAED;;;;;GAKG;AACH,qBAAa,gBAAiB,SAAQ,mBAAmB;IAO3C,OAAO,CAAC,QAAQ,CAAC,MAAM;IANnC,QAAQ,CAAC,IAAI,cAAc;IAC3B,QAAQ,CAAC,IAAI,EAAG,SAAS,CAAU;IAEnC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAS;IAChC,OAAO,CAAC,QAAQ,CAAC,gBAAgB,CAAoB;gBAExB,MAAM,EAAE,sBAAsB;IAsB3D,OAAO,CAAC,qBAAqB;IAmC7B;;;;;;;OAOG;IACH;;OAEG;IACH,OAAO,CAAC,gBAAgB;IASxB;;;;;;;;;;;OAWG;IACH,sBAAsB,CACpB,IAAI,CAAC,EAAE,OAAO,EACd,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EAChC,OAAO,CAAC,EAAE;QAAE,MAAM,CAAC,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAC;QAAC,OAAO,CAAC,EAAE,MAAM,GAAG,MAAM,CAAA;KAAE,GACtE,OAAO;YAsII,OAAO;IA0Bf,aAAa,CAAC,OAAO,EAAE,cAAc,GAAG,OAAO,CAAC,aAAa,CAAC;IAmDpE,4DAA4D;IACtD,cAAc,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,CAAC;IAIzD,iBAAiB,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,CAAC;IA6C5D,aAAa,CAAC,SAAS,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,CAAC;IA4B/E;;;;;;;;;;;;;;OAcG;IACG,aAAa,CACjB,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC9B,OAAO,GAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAM,EACpC,KAAK,GAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAM,EAClC,OAAO,CAAC,EAAE;QAAE,MAAM,CAAC,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAC;QAAC,OAAO,CAAC,EAAE,MAAM,GAAG,MAAM,CAAA;KAAE,GACtE,OAAO,CAAC,aAAa,CAAC;IA+DzB;;;OAGG;IACG,cAAc,CAAC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,OAAO,CAAC,aAAa,CAAC;CAiD5E"}
1
+ {"version":3,"file":"satispay.provider.d.ts","sourceRoot":"","sources":["../../../src/providers/satispay/satispay.provider.ts"],"names":[],"mappings":"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"}
@@ -38,11 +38,22 @@ const crypto = __importStar(require("crypto"));
38
38
  const base_payment_provider_abstract_1 = require("../../abstract/base-payment-provider.abstract");
39
39
  const in_memory_transaction_store_1 = require("../../stores/in-memory-transaction.store");
40
40
  const parsing_util_1 = require("../../utils/parsing.util");
41
+ const ids_1 = require("../../utils/ids");
42
+ const money_1 = require("../../utils/money");
43
+ const url_1 = require("../../utils/url");
44
+ /** Signature headers that `extraHeaders` may never override (compared case-insensitively). */
45
+ const PROTECTED_HEADERS = new Set(['host', 'date', 'digest', 'authorization']);
46
+ const REVOKE_UNSUPPORTED = 'Satispay does not document a merchant-side revocation; stop charging this mandate. ' +
47
+ 'The customer can revoke it from the Satispay app.';
41
48
  /**
42
49
  * Satispay Business API provider.
43
50
  * Flow: `webhook` — creates a MATCH_CODE payment, user pays via Satispay app,
44
51
  * Satispay calls the `callback_url` webhook, which is correlated to the order
45
52
  * via the `ITransactionStore`. The redirect is then verified via `handleRedirect`.
53
+ *
54
+ * Recurring payments (`IMandateProvider`): Automatic Payments, web-redirect flow. A setup creates
55
+ * a pre-authorized payment token, the customer approves it in Satispay, and every charge (the
56
+ * first one included) is a synchronous `PRE_AUTHORIZED` payment. EUR only.
46
57
  */
47
58
  class SatispayProvider extends base_payment_provider_abstract_1.BasePaymentProvider {
48
59
  constructor(config) {
@@ -266,12 +277,59 @@ class SatispayProvider extends base_payment_provider_abstract_1.BasePaymentProvi
266
277
  throw new Error('Invalid provider response');
267
278
  }
268
279
  }
280
+ /**
281
+ * Signed request that never throws and keeps the HTTP status and the parsed body, for the
282
+ * mandate methods (which must tell 4xx from 5xx from a timeout). `extraHeaders` are merged after
283
+ * the signature headers but can never override `Host`, `Date`, `Digest` or `Authorization`
284
+ * (case-insensitively). `request()` keeps its throwing behaviour for the one-off payment flow.
285
+ */
286
+ async send(method, url, body, extraHeaders = {}) {
287
+ try {
288
+ const parsed = new URL(url);
289
+ const headers = this.buildSignatureHeaders(method, parsed.pathname, parsed.host, body);
290
+ for (const [name, value] of Object.entries(extraHeaders)) {
291
+ if (PROTECTED_HEADERS.has(name.toLowerCase()))
292
+ continue;
293
+ headers[name] = value;
294
+ }
295
+ const response = await fetch(url, {
296
+ method,
297
+ headers,
298
+ body: body ? JSON.stringify(body) : undefined,
299
+ signal: AbortSignal.timeout(this.config.timeoutMs ?? 30000),
300
+ });
301
+ let text;
302
+ try {
303
+ text = await response.text();
304
+ }
305
+ catch {
306
+ // The body could not be read (connection reset, timeout while streaming)
307
+ return { ok: false, status: 0, networkError: true };
308
+ }
309
+ const status = typeof response.status === 'number' ? response.status : 0;
310
+ const ok = response.ok === true;
311
+ let json;
312
+ try {
313
+ const value = text ? JSON.parse(text) : undefined;
314
+ if (value !== null && typeof value === 'object' && !Array.isArray(value)) {
315
+ json = value;
316
+ }
317
+ }
318
+ catch {
319
+ json = undefined;
320
+ }
321
+ return json ? { ok, status, json } : { ok, status };
322
+ }
323
+ catch {
324
+ return { ok: false, status: 0, networkError: true };
325
+ }
326
+ }
269
327
  // ---------------------------------------------------------------------------
270
328
  // IPaymentProvider implementation
271
329
  // ---------------------------------------------------------------------------
272
330
  async createPayment(request) {
273
331
  try {
274
- const orderId = request.orderId ?? `ORD-${Date.now()}`;
332
+ const orderId = request.orderId ?? (0, ids_1.generateOrderId)();
275
333
  const baseUrl = this.config.serverUrl ?? '';
276
334
  const callbackPath = this.config.callbackPath ?? '/checkout/satispay/webhook';
277
335
  // Satispay replaces {uuid} at runtime with the actual payment ID
@@ -284,9 +342,18 @@ class SatispayProvider extends base_payment_provider_abstract_1.BasePaymentProvi
284
342
  status: 'FAILED',
285
343
  };
286
344
  }
345
+ // Payment details and refunds are read and sent in hundredths: only 2-decimal currencies
346
+ // keep create, read-back and refund on the same scale
347
+ if ((0, money_1.currencyExponent)(validatedCurrency) !== 2) {
348
+ return {
349
+ success: false,
350
+ error: 'Payment currency is not supported by Satispay: only currencies with 2 decimal places are accepted',
351
+ status: 'FAILED',
352
+ };
353
+ }
287
354
  const body = {
288
355
  flow: 'MATCH_CODE',
289
- amount_unit: Math.round(request.amount * 100),
356
+ amount_unit: (0, money_1.toMinorUnits)(request.amount, validatedCurrency),
290
357
  currency: validatedCurrency,
291
358
  external_code: orderId,
292
359
  callback_url: callbackUrl,
@@ -310,6 +377,9 @@ class SatispayProvider extends base_payment_provider_abstract_1.BasePaymentProvi
310
377
  };
311
378
  }
312
379
  catch (error) {
380
+ if ((0, money_1.isInvalidAmountError)(error)) {
381
+ return { success: false, error: error.message, status: 'FAILED' };
382
+ }
313
383
  return this.errorResult(error, 'Failed to create Satispay payment');
314
384
  }
315
385
  }
@@ -367,7 +437,10 @@ class SatispayProvider extends base_payment_provider_abstract_1.BasePaymentProvi
367
437
  };
368
438
  }
369
439
  try {
370
- const body = amount ? { amount_unit: Math.round(amount * 100) } : {};
440
+ // The refund call carries no currency. createPayment accepts only 2-decimal currencies,
441
+ // so a partial amount is always in hundredths. Only an omitted (undefined) amount means a
442
+ // full refund, as in PayPal: 0 and null are rejected by toMinorUnits like any invalid amount.
443
+ const body = amount !== undefined ? { amount_unit: (0, money_1.toMinorUnits)(amount, 'EUR') } : {};
371
444
  const refund = await this.request('POST', `${this.apiUrl}/payments/${encodeURIComponent(paymentId)}/refunds`, body);
372
445
  return {
373
446
  success: true,
@@ -378,6 +451,9 @@ class SatispayProvider extends base_payment_provider_abstract_1.BasePaymentProvi
378
451
  };
379
452
  }
380
453
  catch (error) {
454
+ if ((0, money_1.isInvalidAmountError)(error)) {
455
+ return { success: false, error: error.message, status: 'FAILED' };
456
+ }
381
457
  return this.errorResult(error, 'Failed to refund Satispay payment');
382
458
  }
383
459
  }
@@ -498,6 +574,367 @@ class SatispayProvider extends base_payment_provider_abstract_1.BasePaymentProvi
498
574
  ...(details.success ? {} : { error: details.error }),
499
575
  };
500
576
  }
577
+ // ---------------------------------------------------------------------------
578
+ // IMandateProvider implementation (Automatic Payments, web-redirect flow)
579
+ // ---------------------------------------------------------------------------
580
+ /**
581
+ * Creates a pre-authorized payment token (`POST /pre_authorized_payment_tokens`) and returns
582
+ * its `redirect_url` as `approvalUrl`. Never open that URL in a WebView.
583
+ *
584
+ * - EUR only.
585
+ * - `callback_url` is `notifyUrl` (or `serverUrl + mandateCallbackPath`) with `mandate_ref`
586
+ * and the raw `token_id={uuid}` placeholder, which Satispay substitutes with the token id.
587
+ * - `redirect_url` is `returnUrl` with `mandate_ref`.
588
+ * - `cancelUrl` is unused: Satispay has no cancel redirect (a canceled authorization is
589
+ * reported through the callback and re-read as `CANCELED`).
590
+ * - `Idempotency-Key: setup-<orderId>`.
591
+ */
592
+ async createMandate(request) {
593
+ if (request.currency !== 'EUR') {
594
+ return { success: false, error: 'Satispay mandates support EUR only' };
595
+ }
596
+ try {
597
+ // The first charge is made later from the pending state: reject an invalid amount now
598
+ (0, money_1.toMinorUnits)(request.amount, 'EUR');
599
+ }
600
+ catch (error) {
601
+ return {
602
+ success: false,
603
+ error: (0, money_1.isInvalidAmountError)(error) ? error.message : 'Invalid mandate amount',
604
+ };
605
+ }
606
+ const base = request.notifyUrl ??
607
+ (this.config.serverUrl
608
+ ? this.config.serverUrl +
609
+ (this.config.mandateCallbackPath ?? '/checkout/satispay/mandate/callback')
610
+ : undefined);
611
+ if (!base) {
612
+ return { success: false, error: 'Satispay mandates require notifyUrl or serverUrl' };
613
+ }
614
+ let callbackUrl;
615
+ let redirectUrl;
616
+ try {
617
+ const callback = new URL((0, url_1.appendMandateRef)(base, request.mandateRef));
618
+ callback.searchParams.delete('token_id');
619
+ // A fragment is never sent to a server; dropping it keeps the placeholder in the query
620
+ callback.hash = '';
621
+ // `{uuid}` is appended raw AFTER serialization: URLSearchParams would percent-encode the
622
+ // braces and Satispay would no longer substitute the token id
623
+ callbackUrl = callback.toString() + '&token_id={uuid}';
624
+ redirectUrl = (0, url_1.appendMandateRef)(request.returnUrl, request.mandateRef);
625
+ }
626
+ catch {
627
+ return { success: false, error: 'Satispay mandate callback or return URL is not an absolute URL' };
628
+ }
629
+ const res = await this.send('POST', `${this.apiUrl}/pre_authorized_payment_tokens`, { reason: request.description, callback_url: callbackUrl, redirect_url: redirectUrl }, { 'Idempotency-Key': 'setup-' + request.orderId });
630
+ const id = res.json?.['id'];
631
+ const approvalUrl = res.json?.['redirect_url'];
632
+ if (!res.ok || typeof id !== 'string' || id === '' || typeof approvalUrl !== 'string' || approvalUrl === '') {
633
+ // Generic: never the response body, which may carry the token id
634
+ return {
635
+ success: false,
636
+ error: res.networkError
637
+ ? 'Satispay authorization could not be created (network error or timeout)'
638
+ : `Satispay authorization could not be created (HTTP ${res.status})`,
639
+ };
640
+ }
641
+ return {
642
+ success: true,
643
+ approvalUrl,
644
+ pending: {
645
+ provider: this.name,
646
+ mandateRef: request.mandateRef,
647
+ orderId: request.orderId,
648
+ amount: request.amount,
649
+ currency: 'EUR',
650
+ providerRef: id,
651
+ createdAt: new Date().toISOString(),
652
+ },
653
+ raw: res.json,
654
+ };
655
+ }
656
+ /**
657
+ * Re-reads the authorization server-to-server (the callback is unsigned) and, once it is
658
+ * `ACCEPTED`, makes the first `PRE_AUTHORIZED` payment with `Idempotency-Key = orderId`. A second
659
+ * confirm (a second callback, or a re-confirm job with `{}`) replays that key and gets Satispay's
660
+ * original response instead of charging twice. The token id is always `pending.providerRef`;
661
+ * a callback `token_id` only has to match it.
662
+ *
663
+ * `CANCELED` is terminal with no mandate. The first payment is replayed with the same body and
664
+ * `Idempotency-Key = orderId` (Satispay returns the original response for a reused key), so a
665
+ * re-confirm after an `unknown` first charge learns whether it was taken:
666
+ * - `ACCEPTED` (the authorization was canceled after the first charge): `success: false`,
667
+ * verified, terminal, `succeeded` first charge, error 'Authorization canceled after the first
668
+ * charge'. The app refunds it or keeps it as a one-off payment.
669
+ * - a 4xx (no payment exists for that key, e.g. the customer declined the authorization):
670
+ * `success: false`, verified, terminal, `failed` first charge ('Authorization canceled').
671
+ * - 401, 403, 408, 409, 429, 5xx, a network error or an unexpected 2xx: unverified, non-terminal,
672
+ * `unknown` first charge.
673
+ * The replay is only authoritative while Satispay keeps the key (retention not documented).
674
+ */
675
+ async confirmMandate(pending, input) {
676
+ const unverified = (error) => ({
677
+ success: false,
678
+ verified: false,
679
+ terminal: false,
680
+ firstCharge: { status: 'unknown', verified: false, chargeRef: pending.orderId },
681
+ error,
682
+ });
683
+ const callbackTokenId = input?.query?.['token_id'];
684
+ if (callbackTokenId !== undefined && callbackTokenId !== pending.providerRef) {
685
+ return unverified('Callback token does not match the pending setup');
686
+ }
687
+ const read = await this.readToken(pending.providerRef);
688
+ if (read.kind !== 'found') {
689
+ return unverified('Satispay authorization could not be read');
690
+ }
691
+ const firstChargeRequest = {
692
+ chargeRef: pending.orderId,
693
+ amount: pending.amount,
694
+ currency: 'EUR',
695
+ };
696
+ switch (read.status) {
697
+ case 'PENDING':
698
+ return {
699
+ success: false,
700
+ verified: true,
701
+ terminal: false,
702
+ error: 'AUTHORIZATION_PENDING',
703
+ firstCharge: {
704
+ status: 'unknown',
705
+ verified: false,
706
+ chargeRef: pending.orderId,
707
+ error: 'Authorization still pending',
708
+ },
709
+ raw: read.json,
710
+ };
711
+ case 'CANCELED': {
712
+ // Replay the first payment exactly as the ACCEPTED branch sends it (same body, same key)
713
+ const replay = await this.postPayment(pending.providerRef, firstChargeRequest);
714
+ if (replay.status === 'succeeded') {
715
+ return {
716
+ success: false,
717
+ verified: true,
718
+ terminal: true,
719
+ error: 'Authorization canceled after the first charge',
720
+ firstCharge: replay,
721
+ raw: read.json,
722
+ };
723
+ }
724
+ if (replay.status === 'failed') {
725
+ // A 4xx: Satispay holds no payment for this key. (A local amount or currency check that
726
+ // failed means no request with this body could ever have been sent.)
727
+ return {
728
+ success: false,
729
+ verified: true,
730
+ terminal: true,
731
+ error: 'Authorization canceled',
732
+ firstCharge: {
733
+ status: 'failed',
734
+ verified: true,
735
+ chargeRef: pending.orderId,
736
+ error: 'Authorization canceled',
737
+ },
738
+ raw: read.json,
739
+ };
740
+ }
741
+ return {
742
+ ...unverified('Authorization canceled; the first charge outcome is unknown'),
743
+ firstCharge: replay,
744
+ };
745
+ }
746
+ case 'ACCEPTED': {
747
+ const mandate = {
748
+ provider: this.name,
749
+ mandateRef: pending.mandateRef,
750
+ providerMandateId: pending.providerRef,
751
+ status: 'active',
752
+ createdAt: new Date().toISOString(),
753
+ };
754
+ const firstCharge = await this.chargeMandate(mandate, firstChargeRequest);
755
+ return { success: true, verified: true, terminal: true, mandate, firstCharge, raw: read.json };
756
+ }
757
+ default:
758
+ return unverified('Satispay authorization status is not recognised');
759
+ }
760
+ }
761
+ /**
762
+ * Merchant-initiated `PRE_AUTHORIZED` payment, synchronous: Satispay creates it `ACCEPTED` or
763
+ * answers 4xx without creating a payment. `Idempotency-Key = chargeRef`: the same `chargeRef`
764
+ * gets Satispay's original response (retention not documented: retry `unknown` promptly).
765
+ * `providerPaymentId` is the Satispay payment id, refundable with `refundPayment`.
766
+ * 408 and 409 are `unknown`, not a refusal: they may answer a concurrent request with the same
767
+ * key that is still in flight (Satispay documents neither the behaviour nor the status).
768
+ */
769
+ async chargeMandate(mandate, charge) {
770
+ const result = await this.postPayment(mandate.providerMandateId, charge);
771
+ if (result.status === 'failed' && result.verified) {
772
+ // A refusal (4xx): tell the app whether the authorization itself is gone
773
+ const read = await this.readToken(mandate.providerMandateId);
774
+ if (read.kind === 'not_found' || (read.kind === 'found' && read.status === 'CANCELED')) {
775
+ result.mandateUnusable = true;
776
+ }
777
+ }
778
+ return result;
779
+ }
780
+ /**
781
+ * `POST /payments` (`PRE_AUTHORIZED`, `Idempotency-Key = chargeRef`) mapped to a `ChargeResult`,
782
+ * without the token re-read that sets `mandateUnusable`. A verified `failed` is always a 4xx.
783
+ */
784
+ async postPayment(tokenId, charge) {
785
+ const chargeRef = charge.chargeRef;
786
+ if (charge.currency !== 'EUR') {
787
+ return { status: 'failed', verified: false, chargeRef, error: 'Satispay mandates support EUR only' };
788
+ }
789
+ let amountUnit;
790
+ try {
791
+ amountUnit = (0, money_1.toMinorUnits)(charge.amount, 'EUR');
792
+ }
793
+ catch (error) {
794
+ return {
795
+ status: 'failed',
796
+ verified: false,
797
+ chargeRef,
798
+ error: (0, money_1.isInvalidAmountError)(error) ? error.message : 'Invalid charge amount',
799
+ };
800
+ }
801
+ const res = await this.send('POST', `${this.apiUrl}/payments`, {
802
+ flow: 'PRE_AUTHORIZED',
803
+ token: tokenId,
804
+ amount_unit: amountUnit,
805
+ currency: 'EUR',
806
+ external_code: chargeRef,
807
+ ...(charge.description ? { metadata: { description: charge.description } } : {}),
808
+ }, { 'Idempotency-Key': chargeRef });
809
+ if (res.networkError) {
810
+ return { status: 'unknown', verified: false, chargeRef, error: 'Satispay charge timed out or failed to connect' };
811
+ }
812
+ if (res.ok) {
813
+ const json = res.json;
814
+ if (!json) {
815
+ return { status: 'unknown', verified: false, chargeRef, error: 'Invalid provider response' };
816
+ }
817
+ const externalCode = json['external_code'];
818
+ if (externalCode !== undefined && externalCode !== null && externalCode !== chargeRef) {
819
+ return {
820
+ status: 'unknown',
821
+ verified: false,
822
+ chargeRef,
823
+ error: 'Satispay payment does not match the charge reference',
824
+ raw: json,
825
+ };
826
+ }
827
+ const paymentId = json['id'];
828
+ if (json['status'] !== 'ACCEPTED' || typeof paymentId !== 'string' || paymentId === '') {
829
+ return {
830
+ status: 'unknown',
831
+ verified: false,
832
+ chargeRef,
833
+ error: `Satispay payment is ${typeof json['status'] === 'string' ? json['status'] : 'unknown'}`,
834
+ raw: json,
835
+ };
836
+ }
837
+ const rawAmount = json['amount_unit'];
838
+ const amount = (0, parsing_util_1.parseAmountFromCents)(rawAmount);
839
+ const currency = json['currency'];
840
+ if ((rawAmount != null && amount === undefined) || (currency != null && typeof currency !== 'string')) {
841
+ return { status: 'unknown', verified: false, chargeRef, error: 'Invalid provider response', raw: json };
842
+ }
843
+ return {
844
+ status: 'succeeded',
845
+ // Authenticated server-to-server response, as in getPaymentDetails
846
+ verified: true,
847
+ chargeRef,
848
+ providerPaymentId: paymentId,
849
+ amount,
850
+ currency: currency ?? undefined,
851
+ raw: json,
852
+ };
853
+ }
854
+ if (res.status === 401 || res.status === 403 || res.status === 429) {
855
+ // Merchant-side (credentials, rate limit): not a refusal of this customer's charge
856
+ return {
857
+ status: 'unknown',
858
+ verified: false,
859
+ chargeRef,
860
+ error: `Satispay rejected the request (HTTP ${res.status})`,
861
+ raw: res.json,
862
+ };
863
+ }
864
+ if (res.status === 408 || res.status === 409) {
865
+ // Possibly a same-key request still in flight: the original may succeed
866
+ return {
867
+ status: 'unknown',
868
+ verified: false,
869
+ chargeRef,
870
+ error: `Satispay charge outcome is unknown (HTTP ${res.status})`,
871
+ raw: res.json,
872
+ };
873
+ }
874
+ if (res.status >= 400 && res.status < 500) {
875
+ const json = res.json;
876
+ return {
877
+ status: 'failed',
878
+ verified: true,
879
+ chargeRef,
880
+ providerCode: String(json?.['code'] ?? res.status),
881
+ error: String(json?.['message'] ?? 'Satispay refused the charge'),
882
+ raw: json,
883
+ };
884
+ }
885
+ return {
886
+ status: 'unknown',
887
+ verified: false,
888
+ chargeRef,
889
+ error: `Satispay charge outcome is unknown (HTTP ${res.status})`,
890
+ raw: res.json,
891
+ };
892
+ }
893
+ /** Re-reads the authorization: ACCEPTED → active, CANCELED or 404 → revoked, PENDING → unknown. */
894
+ async getMandate(mandate) {
895
+ const read = await this.readToken(mandate.providerMandateId);
896
+ if (read.kind === 'not_found')
897
+ return { status: 'revoked', verified: true };
898
+ if (read.kind === 'unavailable')
899
+ return { status: 'unknown', verified: false };
900
+ switch (read.status) {
901
+ case 'ACCEPTED':
902
+ return { status: 'active', verified: true, raw: read.json };
903
+ case 'CANCELED':
904
+ return { status: 'revoked', verified: true, raw: read.json };
905
+ case 'PENDING':
906
+ return { status: 'unknown', verified: true, raw: read.json };
907
+ default:
908
+ return { status: 'unknown', verified: false, raw: read.json };
909
+ }
910
+ }
911
+ /**
912
+ * Makes no network call: Satispay documents no merchant-side revocation (the token API has only
913
+ * GET). The undocumented `PUT /pre_authorized_payment_tokens/{id}` is deliberately not used.
914
+ * Stop charging the mandate; the customer can revoke it from the Satispay app.
915
+ */
916
+ async revokeMandate(_mandate) {
917
+ return { revokedAtProvider: false, error: REVOKE_UNSUPPORTED };
918
+ }
919
+ /**
920
+ * `GET /pre_authorized_payment_tokens/{id}`. A 2xx whose `id` is not `tokenId` or whose `status`
921
+ * is not a string, a network error, a 5xx and any other non-2xx except 404 are `unavailable`.
922
+ */
923
+ async readToken(tokenId) {
924
+ if (typeof tokenId !== 'string' || tokenId === '')
925
+ return { kind: 'unavailable' };
926
+ const res = await this.send('GET', `${this.apiUrl}/pre_authorized_payment_tokens/${encodeURIComponent(tokenId)}`);
927
+ if (res.networkError)
928
+ return { kind: 'unavailable' };
929
+ if (res.status === 404)
930
+ return { kind: 'not_found' };
931
+ if (!res.ok || !res.json)
932
+ return { kind: 'unavailable' };
933
+ const status = res.json['status'];
934
+ if (res.json['id'] !== tokenId || typeof status !== 'string')
935
+ return { kind: 'unavailable' };
936
+ return { kind: 'found', status, json: res.json };
937
+ }
501
938
  }
502
939
  exports.SatispayProvider = SatispayProvider;
503
940
  //# sourceMappingURL=satispay.provider.js.map