awesome-node-checkout 2.0.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.
- package/CHANGELOG.md +35 -0
- package/README.md +280 -2
- package/dist/adapters/express/index.d.ts +69 -1
- package/dist/adapters/express/index.d.ts.map +1 -1
- package/dist/adapters/express/index.js +147 -2
- package/dist/adapters/express/index.js.map +1 -1
- package/dist/checkout-configurator.d.ts +106 -0
- package/dist/checkout-configurator.d.ts.map +1 -1
- package/dist/checkout-configurator.js +515 -0
- package/dist/checkout-configurator.js.map +1 -1
- package/dist/events/checkout-event-bus.d.ts +11 -1
- package/dist/events/checkout-event-bus.d.ts.map +1 -1
- package/dist/events/checkout-event-bus.js.map +1 -1
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +8 -1
- package/dist/index.js.map +1 -1
- package/dist/interfaces/mandate-provider.interface.d.ts +40 -0
- package/dist/interfaces/mandate-provider.interface.d.ts.map +1 -0
- package/dist/interfaces/mandate-provider.interface.js +18 -0
- package/dist/interfaces/mandate-provider.interface.js.map +1 -0
- package/dist/models/checkout-config.model.d.ts +16 -0
- package/dist/models/checkout-config.model.d.ts.map +1 -1
- package/dist/models/errors.d.ts +1 -1
- package/dist/models/errors.d.ts.map +1 -1
- package/dist/models/errors.js.map +1 -1
- package/dist/models/mandate.model.d.ts +166 -0
- package/dist/models/mandate.model.d.ts.map +1 -0
- package/dist/models/mandate.model.js +19 -0
- package/dist/models/mandate.model.js.map +1 -0
- package/dist/providers/nexi/nexi.provider.d.ts +125 -1
- package/dist/providers/nexi/nexi.provider.d.ts.map +1 -1
- package/dist/providers/nexi/nexi.provider.js +652 -10
- package/dist/providers/nexi/nexi.provider.js.map +1 -1
- package/dist/providers/paypal/paypal.provider.d.ts +44 -1
- package/dist/providers/paypal/paypal.provider.d.ts.map +1 -1
- package/dist/providers/paypal/paypal.provider.js +519 -0
- package/dist/providers/paypal/paypal.provider.js.map +1 -1
- package/dist/providers/satispay/satispay.provider.d.ts +86 -1
- package/dist/providers/satispay/satispay.provider.d.ts.map +1 -1
- package/dist/providers/satispay/satispay.provider.js +417 -0
- package/dist/providers/satispay/satispay.provider.js.map +1 -1
- package/dist/utils/redact.d.ts +15 -0
- package/dist/utils/redact.d.ts.map +1 -0
- package/dist/utils/redact.js +60 -0
- package/dist/utils/redact.js.map +1 -0
- package/dist/utils/url.d.ts +12 -0
- package/dist/utils/url.d.ts.map +1 -0
- package/dist/utils/url.js +19 -0
- package/dist/utils/url.js.map +1 -0
- 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;AAMjF,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;
|
|
1
|
+
{"version":3,"file":"satispay.provider.d.ts","sourceRoot":"","sources":["../../../src/providers/satispay/satispay.provider.ts"],"names":[],"mappings":"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"}
|
|
@@ -40,11 +40,20 @@ const in_memory_transaction_store_1 = require("../../stores/in-memory-transactio
|
|
|
40
40
|
const parsing_util_1 = require("../../utils/parsing.util");
|
|
41
41
|
const ids_1 = require("../../utils/ids");
|
|
42
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.';
|
|
43
48
|
/**
|
|
44
49
|
* Satispay Business API provider.
|
|
45
50
|
* Flow: `webhook` — creates a MATCH_CODE payment, user pays via Satispay app,
|
|
46
51
|
* Satispay calls the `callback_url` webhook, which is correlated to the order
|
|
47
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.
|
|
48
57
|
*/
|
|
49
58
|
class SatispayProvider extends base_payment_provider_abstract_1.BasePaymentProvider {
|
|
50
59
|
constructor(config) {
|
|
@@ -268,6 +277,53 @@ class SatispayProvider extends base_payment_provider_abstract_1.BasePaymentProvi
|
|
|
268
277
|
throw new Error('Invalid provider response');
|
|
269
278
|
}
|
|
270
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
|
+
}
|
|
271
327
|
// ---------------------------------------------------------------------------
|
|
272
328
|
// IPaymentProvider implementation
|
|
273
329
|
// ---------------------------------------------------------------------------
|
|
@@ -518,6 +574,367 @@ class SatispayProvider extends base_payment_provider_abstract_1.BasePaymentProvi
|
|
|
518
574
|
...(details.success ? {} : { error: details.error }),
|
|
519
575
|
};
|
|
520
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
|
+
}
|
|
521
938
|
}
|
|
522
939
|
exports.SatispayProvider = SatispayProvider;
|
|
523
940
|
//# sourceMappingURL=satispay.provider.js.map
|