@fire-spark/payments-pagoplux-card 0.13.3 → 0.14.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/dist/index.d.ts +21 -3
- package/dist/index.js +112 -4
- package/package.json +3 -3
package/dist/index.d.ts
CHANGED
|
@@ -73,9 +73,20 @@ type PagopluxOtpSession = {
|
|
|
73
73
|
tknky: string;
|
|
74
74
|
tkniv: string;
|
|
75
75
|
};
|
|
76
|
+
/** Bank challenge of a code 103 with 3DS (API de Pago 1.3 §E-F). */
|
|
77
|
+
type PagopluxChallenge = {
|
|
78
|
+
url: string;
|
|
79
|
+
method: "GET" | "POST";
|
|
80
|
+
parameters: {
|
|
81
|
+
name: string;
|
|
82
|
+
value: string;
|
|
83
|
+
}[];
|
|
84
|
+
version: string | null;
|
|
85
|
+
transaction_id: string | null;
|
|
86
|
+
};
|
|
76
87
|
/** Answer of the Spark `charge` operation. */
|
|
77
88
|
type PagopluxChargeOutcome = {
|
|
78
|
-
status: "PAID" | "FAILED" | "PENDING" | "OTP_REQUIRED";
|
|
89
|
+
status: "PAID" | "FAILED" | "PENDING" | "OTP_REQUIRED" | "THREE_DS_REQUIRED";
|
|
79
90
|
code: number | null;
|
|
80
91
|
description: string | null;
|
|
81
92
|
transaction_id: string | null;
|
|
@@ -90,6 +101,13 @@ type PagopluxChargeOutcome = {
|
|
|
90
101
|
installments: number | null;
|
|
91
102
|
date: string | null;
|
|
92
103
|
otp: PagopluxOtpSession | null;
|
|
104
|
+
/** Only with `THREE_DS_REQUIRED` (absent from older APIs). */
|
|
105
|
+
challenge?: PagopluxChallenge | null;
|
|
106
|
+
};
|
|
107
|
+
/** Answer of the Spark `three_ds_result` operation. */
|
|
108
|
+
type PagopluxThreeDsResult = {
|
|
109
|
+
tender_status: string;
|
|
110
|
+
outcome: PagopluxChargeOutcome | null;
|
|
93
111
|
};
|
|
94
112
|
//#endregion
|
|
95
113
|
//#region src/crypto.d.ts
|
|
@@ -159,7 +177,7 @@ declare const PAGOPLUX_OTP_TRIES = 3;
|
|
|
159
177
|
declare const PAGOPLUX_CARD_POLICY: Partial<PaymentPolicy>;
|
|
160
178
|
//#endregion
|
|
161
179
|
//#region src/errors.d.ts
|
|
162
|
-
/** API de Pago 1.
|
|
180
|
+
/** API de Pago 1.3 §H, keyed by `code` as returned. */
|
|
163
181
|
declare const pagopluxErrors: Readonly<Record<string, import("@fire-spark/payments").PaymentErrorEntry>> & {
|
|
164
182
|
"1": {
|
|
165
183
|
code: "INVALID_CREDENTIALS";
|
|
@@ -206,4 +224,4 @@ declare const pagopluxErrors: Readonly<Record<string, import("@fire-spark/paymen
|
|
|
206
224
|
/** Spark code for a code 2 decline from its message, else `null`. */
|
|
207
225
|
declare const pagopluxDeclineCode: (message: string | null) => "AMBIGUOUS" | "BATCH_CLOSE_REQUIRED" | "CANCELLED_BY_USER" | "CARD_DECLINED" | "EXPIRED_CARD" | "FINANCING_EXPIRED" | "FINANCING_REJECTED" | "FRAUD_SUSPECTED" | "INSUFFICIENT_BALANCE" | "INSUFFICIENT_FUNDS" | "INVALID_CARD" | "INVALID_CREDENTIALS" | "INVALID_PIN" | "INVALID_REQUEST" | "LIMIT_EXCEEDED" | "MANUAL_REVERSAL_REQUIRED" | "NETWORK_ERROR" | "PROVIDER_UNAVAILABLE" | "REVERSAL_NOT_ALLOWED" | "TERMINAL_BUSY" | "TERMINAL_NEEDS_INIT" | "TERMINAL_NOT_CONFIGURED" | "TERMINAL_UNAVAILABLE" | "TIMEOUT" | "TRANSFER_ALREADY_USED" | "TRANSFER_AMOUNT_MISMATCH" | "TRANSFER_NOT_FOUND" | "UNKNOWN" | "USER_TIMEOUT" | "WALLET_EXPIRED" | "WALLET_REJECTED" | null;
|
|
208
226
|
//#endregion
|
|
209
|
-
export { PAGOPLUX_CARD_POLICY, PAGOPLUX_METHOD, PAGOPLUX_OTP_TRIES, PAGOPLUX_PROVIDER, type PagopluxAddressEntry, type PagopluxBuyerEntry, type PagopluxCardCollector, type PagopluxCardEntry, type PagopluxCardForm, type PagopluxCardFormInput, PagopluxCardOptions, type PagopluxChargeOutcome, type PagopluxEncryptedCard, type PagopluxOtpSession, PagopluxRandom, defaultRandom, encryptPagopluxCard, fromBase64, pagopluxAes, pagopluxCardPayments, pagopluxDeclineCode, pagopluxErrors, pagopluxPublicKeyOf, pagopluxRsa, pagopluxSymmetricKey, parsePagopluxPublicKey, toBase64 };
|
|
227
|
+
export { PAGOPLUX_CARD_POLICY, PAGOPLUX_METHOD, PAGOPLUX_OTP_TRIES, PAGOPLUX_PROVIDER, type PagopluxAddressEntry, type PagopluxBuyerEntry, type PagopluxCardCollector, type PagopluxCardEntry, type PagopluxCardForm, type PagopluxCardFormInput, PagopluxCardOptions, type PagopluxChallenge, type PagopluxChargeOutcome, type PagopluxEncryptedCard, type PagopluxOtpSession, PagopluxRandom, type PagopluxThreeDsResult, defaultRandom, encryptPagopluxCard, fromBase64, pagopluxAes, pagopluxCardPayments, pagopluxDeclineCode, pagopluxErrors, pagopluxPublicKeyOf, pagopluxRsa, pagopluxSymmetricKey, parsePagopluxPublicKey, toBase64 };
|
package/dist/index.js
CHANGED
|
@@ -181,7 +181,7 @@ const encryptPagopluxCard = (entry, publicKey, random = defaultRandom) => {
|
|
|
181
181
|
//#region src/errors.ts
|
|
182
182
|
const source = "docs/payments/pagoplux/card.md#11-errores";
|
|
183
183
|
const since = "1.2";
|
|
184
|
-
/** API de Pago 1.
|
|
184
|
+
/** API de Pago 1.3 §H, keyed by `code` as returned. */
|
|
185
185
|
const pagopluxErrors = definePaymentErrors({
|
|
186
186
|
"1": {
|
|
187
187
|
code: "INVALID_CREDENTIALS",
|
|
@@ -427,6 +427,42 @@ const serverError = (ctx, cause) => {
|
|
|
427
427
|
attempt
|
|
428
428
|
});
|
|
429
429
|
};
|
|
430
|
+
/** A UI timeout or an aborted attempt: the orchestrator maps it. */
|
|
431
|
+
var Passthrough = class extends Error {
|
|
432
|
+
inner;
|
|
433
|
+
constructor(inner) {
|
|
434
|
+
super("passthrough");
|
|
435
|
+
this.inner = inner;
|
|
436
|
+
}
|
|
437
|
+
};
|
|
438
|
+
const isUserTimeout = (cause) => {
|
|
439
|
+
return cause instanceof Error && cause.name === "PaymentUserTimeout";
|
|
440
|
+
};
|
|
441
|
+
/**
|
|
442
|
+
* A question to the customer. A template that rejects (closed dialog) is
|
|
443
|
+
* a cancel; a timeout or an aborted attempt goes to the orchestrator.
|
|
444
|
+
*/
|
|
445
|
+
const askUser = async (ctx, call) => {
|
|
446
|
+
try {
|
|
447
|
+
return await call();
|
|
448
|
+
} catch (cause) {
|
|
449
|
+
if (cause instanceof PaymentError) throw cause;
|
|
450
|
+
if (isUserTimeout(cause) || ctx.signal.aborted) throw new Passthrough(cause);
|
|
451
|
+
throw fail("CANCELLED_BY_USER", "The customer closed the prompt");
|
|
452
|
+
}
|
|
453
|
+
};
|
|
454
|
+
/** Bank pages need a moment to land on the 3DS return after the message. */
|
|
455
|
+
const THREE_DS_READS = 4;
|
|
456
|
+
const THREE_DS_READ_GAP_MS = 1500;
|
|
457
|
+
const wait = (ms, signal) => {
|
|
458
|
+
return new Promise((resolve) => {
|
|
459
|
+
const timer = setTimeout(resolve, ms);
|
|
460
|
+
signal.addEventListener("abort", () => {
|
|
461
|
+
clearTimeout(timer);
|
|
462
|
+
resolve();
|
|
463
|
+
});
|
|
464
|
+
});
|
|
465
|
+
};
|
|
430
466
|
const failedResult = (ctx, error) => {
|
|
431
467
|
const m1 = {
|
|
432
468
|
attempt_reference: ctx.attemptReference,
|
|
@@ -488,11 +524,12 @@ const pagopluxCardPayments = (options) => {
|
|
|
488
524
|
text: "Código incorrecto, intenta de nuevo",
|
|
489
525
|
kind: "error"
|
|
490
526
|
});
|
|
491
|
-
const
|
|
527
|
+
const prompt = {
|
|
492
528
|
label: otpLabel,
|
|
493
529
|
type: "number",
|
|
494
530
|
maxLength: 8
|
|
495
|
-
}
|
|
531
|
+
};
|
|
532
|
+
const code = await askUser(ctx, () => ctx.ui.input(prompt));
|
|
496
533
|
const otp = {
|
|
497
534
|
...session,
|
|
498
535
|
code: code.trim()
|
|
@@ -514,6 +551,75 @@ const pagopluxCardPayments = (options) => {
|
|
|
514
551
|
code: 102
|
|
515
552
|
} : outcome;
|
|
516
553
|
};
|
|
554
|
+
/** Outcome the 3DS return recorded on the tender, if it arrived. */
|
|
555
|
+
const readThreeDs = async (ctx, reads) => {
|
|
556
|
+
for (let read = 0; read < reads; read++) {
|
|
557
|
+
if (read > 0) await wait(THREE_DS_READ_GAP_MS, ctx.signal);
|
|
558
|
+
if (ctx.signal.aborted) break;
|
|
559
|
+
const answer = await ctx.server.call("three_ds_result", {});
|
|
560
|
+
if (answer?.outcome) return answer.outcome;
|
|
561
|
+
}
|
|
562
|
+
return null;
|
|
563
|
+
};
|
|
564
|
+
/**
|
|
565
|
+
* Code 103 with a bank page (API de Pago 1.3 §E): the template shows it
|
|
566
|
+
* (`ui.challenge`); the bank sends the customer to the Fire Spark 3DS
|
|
567
|
+
* return, which closes the tender on the server; the device then reads
|
|
568
|
+
* that outcome. Closed before the bank answered: it may have charged.
|
|
569
|
+
*/
|
|
570
|
+
const withChallenge = async (ctx, challenge) => {
|
|
571
|
+
const show = ctx.ui.challenge;
|
|
572
|
+
if (!show) throw fail("INVALID_REQUEST", "The channel cannot show the 3-D Secure challenge");
|
|
573
|
+
ctx.ui.message({
|
|
574
|
+
text: "Verifica el pago con tu banco…",
|
|
575
|
+
kind: "info"
|
|
576
|
+
});
|
|
577
|
+
const c1 = {
|
|
578
|
+
url: challenge.url,
|
|
579
|
+
method: challenge.method
|
|
580
|
+
};
|
|
581
|
+
const c2 = {
|
|
582
|
+
parameters: challenge.parameters,
|
|
583
|
+
version: challenge.version
|
|
584
|
+
};
|
|
585
|
+
const input = {
|
|
586
|
+
...c1,
|
|
587
|
+
...c2,
|
|
588
|
+
signal: ctx.signal
|
|
589
|
+
};
|
|
590
|
+
const shown = await askUser(ctx, () => show.call(ctx.ui, input)).catch((cause) => {
|
|
591
|
+
if (cause instanceof Passthrough) throw cause;
|
|
592
|
+
return { completed: false };
|
|
593
|
+
});
|
|
594
|
+
ctx.ui.message({
|
|
595
|
+
text: "Confirmando pago…",
|
|
596
|
+
kind: "waiting"
|
|
597
|
+
});
|
|
598
|
+
const reads = shown.completed ? THREE_DS_READS : 1;
|
|
599
|
+
const outcome = await readThreeDs(ctx, reads);
|
|
600
|
+
if (outcome) return outcome;
|
|
601
|
+
const pending = {
|
|
602
|
+
code: 103,
|
|
603
|
+
description: "3DS challenge without bank answer",
|
|
604
|
+
transaction_id: challenge.transaction_id
|
|
605
|
+
};
|
|
606
|
+
const e1 = {
|
|
607
|
+
authorization_token: null,
|
|
608
|
+
amount: null,
|
|
609
|
+
card: null
|
|
610
|
+
};
|
|
611
|
+
const e2 = {
|
|
612
|
+
installments: null,
|
|
613
|
+
date: null,
|
|
614
|
+
otp: null
|
|
615
|
+
};
|
|
616
|
+
return {
|
|
617
|
+
status: "PENDING",
|
|
618
|
+
...pending,
|
|
619
|
+
...e1,
|
|
620
|
+
...e2
|
|
621
|
+
};
|
|
622
|
+
};
|
|
517
623
|
const start = async (ctx) => {
|
|
518
624
|
const keyPromise = publicKeyFor(ctx);
|
|
519
625
|
keyPromise.catch(() => void 0);
|
|
@@ -550,9 +656,11 @@ const pagopluxCardPayments = (options) => {
|
|
|
550
656
|
};
|
|
551
657
|
try {
|
|
552
658
|
const first = await charge(ctx, input);
|
|
553
|
-
|
|
659
|
+
let outcome = first.status === "OTP_REQUIRED" ? await withOtp(ctx, input, first) : first;
|
|
660
|
+
if (outcome.status === "THREE_DS_REQUIRED" && outcome.challenge) outcome = await withChallenge(ctx, outcome.challenge);
|
|
554
661
|
return resultOf(ctx, outcome);
|
|
555
662
|
} catch (cause) {
|
|
663
|
+
if (cause instanceof Passthrough) throw cause.inner;
|
|
556
664
|
if (cause instanceof PaymentError) return failedResult(ctx, cause.error);
|
|
557
665
|
return failedResult(ctx, serverError(ctx, cause));
|
|
558
666
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fire-spark/payments-pagoplux-card",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.14.0",
|
|
4
4
|
"description": "Fire Spark PagoPlux card adapter (API de Pago): card encrypted on the device, charged by Spark, OTP for Diners/Interdin/Discover",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"sideEffects": false,
|
|
@@ -18,8 +18,8 @@
|
|
|
18
18
|
},
|
|
19
19
|
"dependencies": {
|
|
20
20
|
"@noble/ciphers": "^1.3.0",
|
|
21
|
-
"@fire-spark/payments": "^0.
|
|
22
|
-
"@fire-spark/types": "^0.
|
|
21
|
+
"@fire-spark/payments": "^0.14.0",
|
|
22
|
+
"@fire-spark/types": "^0.14.0"
|
|
23
23
|
},
|
|
24
24
|
"devDependencies": {
|
|
25
25
|
"tsdown": "^0.22.14",
|