@fire-spark/payments-deuna-widget 0.0.0-stage → 0.11.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 +298 -0
- package/dist/index.js +465 -0
- package/package.json +56 -3
- package/README.md +0 -3
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
import { PaymentAdapter, PaymentEnvironment, PaymentErrorCode, PaymentIntentStatus, PaymentPolicy, StandardPaymentError } from "@fire-spark/payments";
|
|
2
|
+
//#region src/constants.d.ts
|
|
3
|
+
declare const DEUNA_PROVIDER = "deuna";
|
|
4
|
+
/**
|
|
5
|
+
* Widget `paymentMethods` by `payments.config.processor` (same keys as
|
|
6
|
+
* `DEUNA_PROCESSORS_BY_METHOD` of `@repo/validations`). Nequi, Mercado Pago
|
|
7
|
+
* and Deuna App Wallet come from their DEUNA widget pages; PSE uses the
|
|
8
|
+
* processor of the PSE API page and the card entry opens the card form
|
|
9
|
+
* (both to verify in sandbox).
|
|
10
|
+
*/
|
|
11
|
+
declare const DEUNA_PROCESSORS: {
|
|
12
|
+
readonly card: {
|
|
13
|
+
readonly method: "CARD";
|
|
14
|
+
readonly paymentMethod: "credit_card";
|
|
15
|
+
readonly processors: readonly [];
|
|
16
|
+
};
|
|
17
|
+
readonly nequi: {
|
|
18
|
+
readonly method: "WALLET";
|
|
19
|
+
readonly paymentMethod: "voucher";
|
|
20
|
+
readonly processors: readonly ["nequi_push_voucher"];
|
|
21
|
+
};
|
|
22
|
+
readonly mercadopago: {
|
|
23
|
+
readonly method: "WALLET";
|
|
24
|
+
readonly paymentMethod: "wallet";
|
|
25
|
+
readonly processors: readonly ["mercadopago_wallet"];
|
|
26
|
+
};
|
|
27
|
+
readonly deuna_wallet: {
|
|
28
|
+
readonly method: "WALLET";
|
|
29
|
+
readonly paymentMethod: "voucher";
|
|
30
|
+
readonly processors: readonly ["deuna_wallet"];
|
|
31
|
+
};
|
|
32
|
+
readonly pse: {
|
|
33
|
+
readonly method: "BANK_TRANSFER";
|
|
34
|
+
readonly paymentMethod: "bank_transfer";
|
|
35
|
+
readonly processors: readonly ["payu_pse"];
|
|
36
|
+
};
|
|
37
|
+
};
|
|
38
|
+
type DeunaProcessor = keyof typeof DEUNA_PROCESSORS;
|
|
39
|
+
declare const DEUNA_PROCESSOR_KEYS: DeunaProcessor[];
|
|
40
|
+
/**
|
|
41
|
+
* `start` covers the whole widget session (push approval, bank redirect),
|
|
42
|
+
* so the provider timeout is the longest validity DEUNA documents for a
|
|
43
|
+
* customer step (15 min QR). Dashboard `runtime` still wins.
|
|
44
|
+
*/
|
|
45
|
+
declare const DEUNA_WIDGET_POLICY: Partial<PaymentPolicy>;
|
|
46
|
+
/** `ctx.storage` key that keeps the DEUNA order token of an attempt. */
|
|
47
|
+
declare const orderTokenKey: (attemptReference: string) => string;
|
|
48
|
+
//#endregion
|
|
49
|
+
//#region src/types.d.ts
|
|
50
|
+
/** Methods this package charges through the DEUNA Payment Widget. */
|
|
51
|
+
type DeunaWidgetMethod = "CARD" | "WALLET" | "BANK_TRANSFER";
|
|
52
|
+
/** `paymentMethods` entry of `initPaymentWidget` (the engine takes one). */
|
|
53
|
+
type DeunaWidgetPaymentMethod = {
|
|
54
|
+
paymentMethod: string;
|
|
55
|
+
/** Omitted for cards: DEUNA routes them to the merchant's processor. */
|
|
56
|
+
processors?: string[];
|
|
57
|
+
};
|
|
58
|
+
type DeunaWidgetLanguage = "es" | "en" | "pt";
|
|
59
|
+
type DeunaWidgetOpenInput = {
|
|
60
|
+
publicApiKey: string;
|
|
61
|
+
environment: PaymentEnvironment;
|
|
62
|
+
orderToken: string;
|
|
63
|
+
paymentMethods: DeunaWidgetPaymentMethod[];
|
|
64
|
+
language: DeunaWidgetLanguage;
|
|
65
|
+
/** Aborted on timeout or cancel: close the widget, resolve `closed`. */
|
|
66
|
+
signal: AbortSignal;
|
|
67
|
+
};
|
|
68
|
+
/** How the widget ended, one variant per DEUNA SDK callback. */
|
|
69
|
+
type DeunaWidgetOutcome =
|
|
70
|
+
/** `onSuccess`: the payment went through (confirmed with `query`). */
|
|
71
|
+
{
|
|
72
|
+
type: "success";
|
|
73
|
+
order?: Record<string, unknown>;
|
|
74
|
+
} |
|
|
75
|
+
/** `onPending`: the customer still has to approve (push, bank, app). */
|
|
76
|
+
{
|
|
77
|
+
type: "pending";
|
|
78
|
+
order?: Record<string, unknown>;
|
|
79
|
+
} |
|
|
80
|
+
/** `onExpired`: the code, QR or push expired. */
|
|
81
|
+
{
|
|
82
|
+
type: "expired";
|
|
83
|
+
} |
|
|
84
|
+
/** `onError`: `code` from the error payload, `errorType` from the SDK. */
|
|
85
|
+
{
|
|
86
|
+
type: "error";
|
|
87
|
+
code?: string | null;
|
|
88
|
+
errorType?: string | null;
|
|
89
|
+
message?: string | null;
|
|
90
|
+
} |
|
|
91
|
+
/** `onClosed`: dismissed by the customer or by `close()`. */
|
|
92
|
+
{
|
|
93
|
+
type: "closed";
|
|
94
|
+
by: "user" | "system";
|
|
95
|
+
};
|
|
96
|
+
/**
|
|
97
|
+
* Implemented once per template with the DEUNA SDK
|
|
98
|
+
* (`@deuna/react-native-sdk` or the Web SDK). It opens the widget with the
|
|
99
|
+
* given order token and resolves with the first final callback.
|
|
100
|
+
*/
|
|
101
|
+
type DeunaWidgetBridge = {
|
|
102
|
+
open(input: DeunaWidgetOpenInput): Promise<DeunaWidgetOutcome>;
|
|
103
|
+
};
|
|
104
|
+
/** `payment.data.status` of a DEUNA order (docs: payment-workflow-statuses). */
|
|
105
|
+
type DeunaPaymentStatus = "pending" | "pending_3ds" | "processing" | "processed" | "authorizing" | "authorized" | "capturing" | "partial_capturing" | "partial_captured" | "captured" | "voiding" | "voided" | "refunding" | "partial_refunding" | "partial_refunded" | "refunded" | "manual_review" | "denied" | "cancelled" | "expired";
|
|
106
|
+
/** Where DEUNA sends the customer back (Mercado Pago, PSE); deep links ok. */
|
|
107
|
+
type DeunaCallbackUrls = Partial<Record<"on_success" | "on_pending" | "on_failed" | "on_canceled" | "on_reject", string>>;
|
|
108
|
+
type DeunaTokenizeInput = {
|
|
109
|
+
paymentId: string;
|
|
110
|
+
orderId: string;
|
|
111
|
+
attemptReference: string;
|
|
112
|
+
callbackUrls?: DeunaCallbackUrls;
|
|
113
|
+
};
|
|
114
|
+
/** `order_id` is the attempt reference (our DEUNA `order.order_id`). */
|
|
115
|
+
type DeunaTokenizeOutput = {
|
|
116
|
+
order_token: string;
|
|
117
|
+
order_id: string;
|
|
118
|
+
};
|
|
119
|
+
/** Payment status of a DEUNA order, normalized by the Spark API. */
|
|
120
|
+
type DeunaOrderState = {
|
|
121
|
+
order_token: string;
|
|
122
|
+
status: DeunaPaymentStatus | (string & {});
|
|
123
|
+
transaction_id?: string | null;
|
|
124
|
+
/** DEUNA `error.code` (core, `DP-`, `EMA-`…) when the payment failed. */
|
|
125
|
+
error?: {
|
|
126
|
+
code: string | null;
|
|
127
|
+
message: string | null;
|
|
128
|
+
} | null;
|
|
129
|
+
};
|
|
130
|
+
/**
|
|
131
|
+
* Spark DEUNA endpoints (`/storefront/v1/payments/deuna/:payment_id/...`).
|
|
132
|
+
* `createDeunaWidgetApi` of `@fire-spark/storefront` implements it; the
|
|
133
|
+
* private key never leaves the API.
|
|
134
|
+
*/
|
|
135
|
+
type DeunaWidgetApi = {
|
|
136
|
+
/** `POST .../tokenize`: DEUNA order for the tender attempt. */
|
|
137
|
+
tokenize(input: DeunaTokenizeInput): Promise<DeunaTokenizeOutput>;
|
|
138
|
+
/** `GET .../orders/:order_token`: payment status. */
|
|
139
|
+
order(input: {
|
|
140
|
+
paymentId: string;
|
|
141
|
+
orderToken: string;
|
|
142
|
+
}): Promise<DeunaOrderState>;
|
|
143
|
+
};
|
|
144
|
+
//#endregion
|
|
145
|
+
//#region src/adapter.d.ts
|
|
146
|
+
type DeunaWidgetOptions = {
|
|
147
|
+
method: DeunaWidgetMethod;
|
|
148
|
+
/** Template implementation of the DEUNA Payment Widget. */
|
|
149
|
+
bridge: DeunaWidgetBridge;
|
|
150
|
+
/** Spark DEUNA endpoints: `createDeunaWidgetApi` of the storefront SDK. */
|
|
151
|
+
api: DeunaWidgetApi;
|
|
152
|
+
language?: DeunaWidgetLanguage;
|
|
153
|
+
/** Return URLs or deep links for redirect methods (Mercado Pago, PSE). */
|
|
154
|
+
callbackUrls?: DeunaCallbackUrls;
|
|
155
|
+
};
|
|
156
|
+
/** Processors this method can charge with. */
|
|
157
|
+
declare const deunaProcessorsFor: (method: DeunaWidgetMethod) => ("card" | "deuna_wallet" | "mercadopago" | "nequi" | "pse")[];
|
|
158
|
+
/** `payments.config.processor`; optional when the method has only one. */
|
|
159
|
+
declare const resolveDeunaProcessor: (method: DeunaWidgetMethod, config: Record<string, unknown>) => DeunaProcessor;
|
|
160
|
+
/** `paymentMethods` of `initPaymentWidget` for a processor. */
|
|
161
|
+
declare const deunaWidgetPaymentMethods: (processor: DeunaProcessor) => DeunaWidgetPaymentMethod[];
|
|
162
|
+
/**
|
|
163
|
+
* DEUNA Payment Widget for every method (card, Nequi, Mercado Pago, Deuna
|
|
164
|
+
* App Wallet, PSE). Spark tokenizes the DEUNA order with the private key
|
|
165
|
+
* (`api.tokenize`), the template opens the widget with the public key, and
|
|
166
|
+
* the order status read back from Spark (`api.order`) is the one that
|
|
167
|
+
* counts. Refunds go through the DEUNA Admin (manual reversal).
|
|
168
|
+
*/
|
|
169
|
+
declare const deunaWidgetPayments: (options: DeunaWidgetOptions) => PaymentAdapter;
|
|
170
|
+
//#endregion
|
|
171
|
+
//#region src/errors.d.ts
|
|
172
|
+
/**
|
|
173
|
+
* DEUNA codes as returned: core API errors, widget/SDK error types and the
|
|
174
|
+
* `DP-` processor codes a wallet or bank payment can hit.
|
|
175
|
+
*/
|
|
176
|
+
declare const deunaErrors: Readonly<Record<string, import("@fire-spark/payments").PaymentErrorEntry>> & {
|
|
177
|
+
PAYMENT_DECLINED: {
|
|
178
|
+
code: "WALLET_REJECTED";
|
|
179
|
+
source: string;
|
|
180
|
+
since: string;
|
|
181
|
+
};
|
|
182
|
+
PAYMENT_FAILED: {
|
|
183
|
+
code: "WALLET_REJECTED";
|
|
184
|
+
source: string;
|
|
185
|
+
since: string;
|
|
186
|
+
};
|
|
187
|
+
PAYMENT_ERROR: {
|
|
188
|
+
code: "WALLET_REJECTED";
|
|
189
|
+
source: string;
|
|
190
|
+
since: string;
|
|
191
|
+
};
|
|
192
|
+
INITIALIZATION_FAILED: {
|
|
193
|
+
code: "PROVIDER_UNAVAILABLE";
|
|
194
|
+
retryable: true;
|
|
195
|
+
source: string;
|
|
196
|
+
since: string;
|
|
197
|
+
};
|
|
198
|
+
CARD_EXPIRED: {
|
|
199
|
+
code: "EXPIRED_CARD";
|
|
200
|
+
source: string;
|
|
201
|
+
since: string;
|
|
202
|
+
};
|
|
203
|
+
INVALID_REQUEST: {
|
|
204
|
+
code: "INVALID_REQUEST";
|
|
205
|
+
source: string;
|
|
206
|
+
since: string;
|
|
207
|
+
};
|
|
208
|
+
AUTHENTICATION_FAILED: {
|
|
209
|
+
code: "INVALID_CREDENTIALS";
|
|
210
|
+
source: string;
|
|
211
|
+
since: string;
|
|
212
|
+
};
|
|
213
|
+
RESOURCE_NOT_FOUND: {
|
|
214
|
+
code: "INVALID_REQUEST";
|
|
215
|
+
source: string;
|
|
216
|
+
since: string;
|
|
217
|
+
};
|
|
218
|
+
IDEMPOTENCY_CONFLICT: {
|
|
219
|
+
code: "AMBIGUOUS";
|
|
220
|
+
source: string;
|
|
221
|
+
since: string;
|
|
222
|
+
};
|
|
223
|
+
RATE_LIMIT_EXCEEDED: {
|
|
224
|
+
code: "PROVIDER_UNAVAILABLE";
|
|
225
|
+
retryable: true;
|
|
226
|
+
source: string;
|
|
227
|
+
since: string;
|
|
228
|
+
};
|
|
229
|
+
PROVIDER_UNAVAILABLE: {
|
|
230
|
+
code: "PROVIDER_UNAVAILABLE";
|
|
231
|
+
retryable: true;
|
|
232
|
+
source: string;
|
|
233
|
+
since: string;
|
|
234
|
+
};
|
|
235
|
+
"DP-4200": {
|
|
236
|
+
code: "PROVIDER_UNAVAILABLE";
|
|
237
|
+
retryable: true;
|
|
238
|
+
source: string;
|
|
239
|
+
since: string;
|
|
240
|
+
};
|
|
241
|
+
"DP-4504": {
|
|
242
|
+
code: "PROVIDER_UNAVAILABLE";
|
|
243
|
+
retryable: true;
|
|
244
|
+
source: string;
|
|
245
|
+
since: string;
|
|
246
|
+
};
|
|
247
|
+
"DP-4507": {
|
|
248
|
+
code: "INVALID_PIN";
|
|
249
|
+
source: string;
|
|
250
|
+
since: string;
|
|
251
|
+
};
|
|
252
|
+
"DP-4520": {
|
|
253
|
+
code: "WALLET_REJECTED";
|
|
254
|
+
source: string;
|
|
255
|
+
since: string;
|
|
256
|
+
};
|
|
257
|
+
"DP-4562": {
|
|
258
|
+
code: "WALLET_EXPIRED";
|
|
259
|
+
source: string;
|
|
260
|
+
since: string;
|
|
261
|
+
};
|
|
262
|
+
"DP-9000": {
|
|
263
|
+
code: "INSUFFICIENT_FUNDS";
|
|
264
|
+
source: string;
|
|
265
|
+
since: string;
|
|
266
|
+
};
|
|
267
|
+
"DP-9001": {
|
|
268
|
+
code: "LIMIT_EXCEEDED";
|
|
269
|
+
source: string;
|
|
270
|
+
since: string;
|
|
271
|
+
};
|
|
272
|
+
"DP-9999": {
|
|
273
|
+
code: "UNKNOWN";
|
|
274
|
+
source: string;
|
|
275
|
+
since: string;
|
|
276
|
+
};
|
|
277
|
+
};
|
|
278
|
+
//#endregion
|
|
279
|
+
//#region src/status.d.ts
|
|
280
|
+
/** What a DEUNA payment status means for the tender. */
|
|
281
|
+
type DeunaStatusMapping = {
|
|
282
|
+
status: PaymentIntentStatus;
|
|
283
|
+
/** Spark code of a FAILED status. */
|
|
284
|
+
error?: PaymentErrorCode;
|
|
285
|
+
};
|
|
286
|
+
/** Spark code of a rejected payment of `method`. */
|
|
287
|
+
declare const deunaDeclinedCode: (method: DeunaWidgetMethod) => PaymentErrorCode;
|
|
288
|
+
/** `payment.data.status` (case-sensitive) to the tender status. */
|
|
289
|
+
declare const mapDeunaStatus: (status: string, method: DeunaWidgetMethod) => DeunaStatusMapping;
|
|
290
|
+
/** Error of a FAILED state: the DEUNA code wins over the status default. */
|
|
291
|
+
declare const deunaStateError: (input: {
|
|
292
|
+
method: DeunaWidgetMethod;
|
|
293
|
+
state: DeunaOrderState;
|
|
294
|
+
fallback: PaymentErrorCode;
|
|
295
|
+
attempt: number;
|
|
296
|
+
}) => StandardPaymentError;
|
|
297
|
+
//#endregion
|
|
298
|
+
export { DEUNA_PROCESSORS, DEUNA_PROCESSOR_KEYS, DEUNA_PROVIDER, DEUNA_WIDGET_POLICY, type DeunaCallbackUrls, type DeunaOrderState, type DeunaPaymentStatus, DeunaProcessor, DeunaStatusMapping, type DeunaTokenizeInput, type DeunaTokenizeOutput, type DeunaWidgetApi, type DeunaWidgetBridge, type DeunaWidgetLanguage, type DeunaWidgetMethod, type DeunaWidgetOpenInput, DeunaWidgetOptions, type DeunaWidgetOutcome, type DeunaWidgetPaymentMethod, deunaDeclinedCode, deunaErrors, deunaProcessorsFor, deunaStateError, deunaWidgetPaymentMethods, deunaWidgetPayments, mapDeunaStatus, orderTokenKey, resolveDeunaProcessor };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,465 @@
|
|
|
1
|
+
import { PaymentError, createPaymentError, definePaymentErrors, toPaymentError } from "@fire-spark/payments";
|
|
2
|
+
//#region src/constants.ts
|
|
3
|
+
const DEUNA_PROVIDER = "deuna";
|
|
4
|
+
/**
|
|
5
|
+
* Widget `paymentMethods` by `payments.config.processor` (same keys as
|
|
6
|
+
* `DEUNA_PROCESSORS_BY_METHOD` of `@repo/validations`). Nequi, Mercado Pago
|
|
7
|
+
* and Deuna App Wallet come from their DEUNA widget pages; PSE uses the
|
|
8
|
+
* processor of the PSE API page and the card entry opens the card form
|
|
9
|
+
* (both to verify in sandbox).
|
|
10
|
+
*/
|
|
11
|
+
const DEUNA_PROCESSORS = {
|
|
12
|
+
card: {
|
|
13
|
+
method: "CARD",
|
|
14
|
+
paymentMethod: "credit_card",
|
|
15
|
+
processors: []
|
|
16
|
+
},
|
|
17
|
+
nequi: {
|
|
18
|
+
method: "WALLET",
|
|
19
|
+
paymentMethod: "voucher",
|
|
20
|
+
processors: ["nequi_push_voucher"]
|
|
21
|
+
},
|
|
22
|
+
mercadopago: {
|
|
23
|
+
method: "WALLET",
|
|
24
|
+
paymentMethod: "wallet",
|
|
25
|
+
processors: ["mercadopago_wallet"]
|
|
26
|
+
},
|
|
27
|
+
deuna_wallet: {
|
|
28
|
+
method: "WALLET",
|
|
29
|
+
paymentMethod: "voucher",
|
|
30
|
+
processors: ["deuna_wallet"]
|
|
31
|
+
},
|
|
32
|
+
pse: {
|
|
33
|
+
method: "BANK_TRANSFER",
|
|
34
|
+
paymentMethod: "bank_transfer",
|
|
35
|
+
processors: ["payu_pse"]
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
const DEUNA_PROCESSOR_KEYS = Object.keys(DEUNA_PROCESSORS);
|
|
39
|
+
/**
|
|
40
|
+
* `start` covers the whole widget session (push approval, bank redirect),
|
|
41
|
+
* so the provider timeout is the longest validity DEUNA documents for a
|
|
42
|
+
* customer step (15 min QR). Dashboard `runtime` still wins.
|
|
43
|
+
*/
|
|
44
|
+
const DEUNA_WIDGET_POLICY = {
|
|
45
|
+
timeout_ms: 9e5,
|
|
46
|
+
poll_interval_ms: 3e3,
|
|
47
|
+
qr_ttl_ms: 18e4
|
|
48
|
+
};
|
|
49
|
+
/** `ctx.storage` key that keeps the DEUNA order token of an attempt. */
|
|
50
|
+
const orderTokenKey = (attemptReference) => {
|
|
51
|
+
return `deuna:order_token:${attemptReference}`;
|
|
52
|
+
};
|
|
53
|
+
//#endregion
|
|
54
|
+
//#region src/errors.ts
|
|
55
|
+
const source = "docs/payments/deuna/widget.md#11-errores";
|
|
56
|
+
/**
|
|
57
|
+
* DEUNA codes as returned: core API errors, widget/SDK error types and the
|
|
58
|
+
* `DP-` processor codes a wallet or bank payment can hit.
|
|
59
|
+
*/
|
|
60
|
+
const deunaErrors = definePaymentErrors({
|
|
61
|
+
PAYMENT_DECLINED: {
|
|
62
|
+
code: "WALLET_REJECTED",
|
|
63
|
+
source,
|
|
64
|
+
since: "v2"
|
|
65
|
+
},
|
|
66
|
+
PAYMENT_FAILED: {
|
|
67
|
+
code: "WALLET_REJECTED",
|
|
68
|
+
source,
|
|
69
|
+
since: "v2"
|
|
70
|
+
},
|
|
71
|
+
PAYMENT_ERROR: {
|
|
72
|
+
code: "WALLET_REJECTED",
|
|
73
|
+
source,
|
|
74
|
+
since: "v2"
|
|
75
|
+
},
|
|
76
|
+
INITIALIZATION_FAILED: {
|
|
77
|
+
code: "PROVIDER_UNAVAILABLE",
|
|
78
|
+
retryable: true,
|
|
79
|
+
source,
|
|
80
|
+
since: "v2"
|
|
81
|
+
},
|
|
82
|
+
CARD_EXPIRED: {
|
|
83
|
+
code: "EXPIRED_CARD",
|
|
84
|
+
source,
|
|
85
|
+
since: "v2"
|
|
86
|
+
},
|
|
87
|
+
INVALID_REQUEST: {
|
|
88
|
+
code: "INVALID_REQUEST",
|
|
89
|
+
source,
|
|
90
|
+
since: "v2"
|
|
91
|
+
},
|
|
92
|
+
AUTHENTICATION_FAILED: {
|
|
93
|
+
code: "INVALID_CREDENTIALS",
|
|
94
|
+
source,
|
|
95
|
+
since: "v2"
|
|
96
|
+
},
|
|
97
|
+
RESOURCE_NOT_FOUND: {
|
|
98
|
+
code: "INVALID_REQUEST",
|
|
99
|
+
source,
|
|
100
|
+
since: "v2"
|
|
101
|
+
},
|
|
102
|
+
IDEMPOTENCY_CONFLICT: {
|
|
103
|
+
code: "AMBIGUOUS",
|
|
104
|
+
source,
|
|
105
|
+
since: "v2"
|
|
106
|
+
},
|
|
107
|
+
RATE_LIMIT_EXCEEDED: {
|
|
108
|
+
code: "PROVIDER_UNAVAILABLE",
|
|
109
|
+
retryable: true,
|
|
110
|
+
source,
|
|
111
|
+
since: "v2"
|
|
112
|
+
},
|
|
113
|
+
PROVIDER_UNAVAILABLE: {
|
|
114
|
+
code: "PROVIDER_UNAVAILABLE",
|
|
115
|
+
retryable: true,
|
|
116
|
+
source,
|
|
117
|
+
since: "v2"
|
|
118
|
+
},
|
|
119
|
+
"DP-4200": {
|
|
120
|
+
code: "PROVIDER_UNAVAILABLE",
|
|
121
|
+
retryable: true,
|
|
122
|
+
source,
|
|
123
|
+
since: "v2"
|
|
124
|
+
},
|
|
125
|
+
"DP-4504": {
|
|
126
|
+
code: "PROVIDER_UNAVAILABLE",
|
|
127
|
+
retryable: true,
|
|
128
|
+
source,
|
|
129
|
+
since: "v2"
|
|
130
|
+
},
|
|
131
|
+
"DP-4507": {
|
|
132
|
+
code: "INVALID_PIN",
|
|
133
|
+
source,
|
|
134
|
+
since: "v2"
|
|
135
|
+
},
|
|
136
|
+
"DP-4520": {
|
|
137
|
+
code: "WALLET_REJECTED",
|
|
138
|
+
source,
|
|
139
|
+
since: "v2"
|
|
140
|
+
},
|
|
141
|
+
"DP-4562": {
|
|
142
|
+
code: "WALLET_EXPIRED",
|
|
143
|
+
source,
|
|
144
|
+
since: "v2"
|
|
145
|
+
},
|
|
146
|
+
"DP-9000": {
|
|
147
|
+
code: "INSUFFICIENT_FUNDS",
|
|
148
|
+
source,
|
|
149
|
+
since: "v2"
|
|
150
|
+
},
|
|
151
|
+
"DP-9001": {
|
|
152
|
+
code: "LIMIT_EXCEEDED",
|
|
153
|
+
source,
|
|
154
|
+
since: "v2"
|
|
155
|
+
},
|
|
156
|
+
"DP-9999": {
|
|
157
|
+
code: "UNKNOWN",
|
|
158
|
+
source,
|
|
159
|
+
since: "v2"
|
|
160
|
+
}
|
|
161
|
+
});
|
|
162
|
+
//#endregion
|
|
163
|
+
//#region src/status.ts
|
|
164
|
+
const PAID = [
|
|
165
|
+
"processed",
|
|
166
|
+
"authorized",
|
|
167
|
+
"captured",
|
|
168
|
+
"partial_captured"
|
|
169
|
+
];
|
|
170
|
+
const PENDING = [
|
|
171
|
+
"pending",
|
|
172
|
+
"pending_3ds",
|
|
173
|
+
"processing",
|
|
174
|
+
"authorizing",
|
|
175
|
+
"capturing",
|
|
176
|
+
"partial_capturing",
|
|
177
|
+
"manual_review"
|
|
178
|
+
];
|
|
179
|
+
const CANCELLED = [
|
|
180
|
+
"cancelled",
|
|
181
|
+
"voiding",
|
|
182
|
+
"voided"
|
|
183
|
+
];
|
|
184
|
+
const REFUNDED = ["refunding", "refunded"];
|
|
185
|
+
const PARTIALLY_REFUNDED = ["partial_refunding", "partial_refunded"];
|
|
186
|
+
/** Spark code of a rejected payment of `method`. */
|
|
187
|
+
const deunaDeclinedCode = (method) => {
|
|
188
|
+
return method === "CARD" ? "CARD_DECLINED" : "WALLET_REJECTED";
|
|
189
|
+
};
|
|
190
|
+
/** `payment.data.status` (case-sensitive) to the tender status. */
|
|
191
|
+
const mapDeunaStatus = (status, method) => {
|
|
192
|
+
if (PAID.includes(status)) return { status: "PAID" };
|
|
193
|
+
if (PENDING.includes(status)) return { status: "PENDING" };
|
|
194
|
+
if (CANCELLED.includes(status)) return { status: "CANCELLED" };
|
|
195
|
+
if (REFUNDED.includes(status)) return { status: "REFUNDED" };
|
|
196
|
+
if (PARTIALLY_REFUNDED.includes(status)) return { status: "PAID" };
|
|
197
|
+
const failed = (error) => {
|
|
198
|
+
return {
|
|
199
|
+
status: "FAILED",
|
|
200
|
+
error
|
|
201
|
+
};
|
|
202
|
+
};
|
|
203
|
+
if (status === "denied") return failed(deunaDeclinedCode(method));
|
|
204
|
+
if (status === "expired") return failed("WALLET_EXPIRED");
|
|
205
|
+
return failed("AMBIGUOUS");
|
|
206
|
+
};
|
|
207
|
+
const BANK_TRANSFER_DECLINED = "Bank transfer rejected";
|
|
208
|
+
/** Error of a FAILED state: the DEUNA code wins over the status default. */
|
|
209
|
+
const deunaStateError = (input) => {
|
|
210
|
+
const { method, state, fallback, attempt } = input;
|
|
211
|
+
const providerError = state.error ?? null;
|
|
212
|
+
if (providerError?.code) return toPaymentError(deunaErrors, {
|
|
213
|
+
code: providerError.code,
|
|
214
|
+
fallback,
|
|
215
|
+
attempt,
|
|
216
|
+
message: providerError.message
|
|
217
|
+
});
|
|
218
|
+
const extra = {
|
|
219
|
+
attempt,
|
|
220
|
+
provider_code: state.status
|
|
221
|
+
};
|
|
222
|
+
if (method !== "BANK_TRANSFER" || fallback !== "WALLET_REJECTED") return createPaymentError(fallback, extra);
|
|
223
|
+
const message = BANK_TRANSFER_DECLINED;
|
|
224
|
+
return createPaymentError(fallback, {
|
|
225
|
+
...extra,
|
|
226
|
+
message
|
|
227
|
+
});
|
|
228
|
+
};
|
|
229
|
+
//#endregion
|
|
230
|
+
//#region src/adapter.ts
|
|
231
|
+
const record = (value) => {
|
|
232
|
+
if (!value || typeof value !== "object") return {};
|
|
233
|
+
return value;
|
|
234
|
+
};
|
|
235
|
+
const fail = (code, message) => {
|
|
236
|
+
return new PaymentError(createPaymentError(code, { provider_message: message }));
|
|
237
|
+
};
|
|
238
|
+
/** Processors this method can charge with. */
|
|
239
|
+
const deunaProcessorsFor = (method) => {
|
|
240
|
+
return DEUNA_PROCESSOR_KEYS.filter((key) => {
|
|
241
|
+
return DEUNA_PROCESSORS[key].method === method;
|
|
242
|
+
});
|
|
243
|
+
};
|
|
244
|
+
/** `payments.config.processor`; optional when the method has only one. */
|
|
245
|
+
const resolveDeunaProcessor = (method, config) => {
|
|
246
|
+
const allowed = deunaProcessorsFor(method);
|
|
247
|
+
const value = config.processor;
|
|
248
|
+
const known = allowed.find((key) => key === value);
|
|
249
|
+
if (known) return known;
|
|
250
|
+
const only = allowed.length === 1 ? allowed[0] : void 0;
|
|
251
|
+
if (value == null && only) return only;
|
|
252
|
+
const message = `Unsupported DEUNA processor for ${method}: ${String(value)}`;
|
|
253
|
+
throw fail("INVALID_REQUEST", message);
|
|
254
|
+
};
|
|
255
|
+
/** `paymentMethods` of `initPaymentWidget` for a processor. */
|
|
256
|
+
const deunaWidgetPaymentMethods = (processor) => {
|
|
257
|
+
const entry = DEUNA_PROCESSORS[processor];
|
|
258
|
+
const base = { paymentMethod: entry.paymentMethod };
|
|
259
|
+
if (!entry.processors.length) return [base];
|
|
260
|
+
return [{
|
|
261
|
+
...base,
|
|
262
|
+
processors: [...entry.processors]
|
|
263
|
+
}];
|
|
264
|
+
};
|
|
265
|
+
const publicKeyOf = (config) => {
|
|
266
|
+
const key = config.public_key;
|
|
267
|
+
if (typeof key === "string" && key.trim()) return key.trim();
|
|
268
|
+
throw fail("INVALID_CREDENTIALS", "Missing DEUNA public_key");
|
|
269
|
+
};
|
|
270
|
+
/** Card brand and last 4 are not exposed by the widget: no card details. */
|
|
271
|
+
const detailsOf = (charge, reference) => {
|
|
272
|
+
if (charge.method === "CARD") return null;
|
|
273
|
+
if (charge.method === "BANK_TRANSFER") return {
|
|
274
|
+
method: "BANK_TRANSFER",
|
|
275
|
+
reference
|
|
276
|
+
};
|
|
277
|
+
return {
|
|
278
|
+
method: "WALLET",
|
|
279
|
+
wallet: charge.processor,
|
|
280
|
+
reference
|
|
281
|
+
};
|
|
282
|
+
};
|
|
283
|
+
/** Tender result for a DEUNA order state. */
|
|
284
|
+
const toResult = (ctx, charge, state, base) => {
|
|
285
|
+
const mapped = mapDeunaStatus(state.status, charge.method);
|
|
286
|
+
const transactionId = state.transaction_id ?? null;
|
|
287
|
+
const previous = record(base?.metadata.provider);
|
|
288
|
+
const p1 = {
|
|
289
|
+
order_token: state.order_token,
|
|
290
|
+
processor: charge.processor
|
|
291
|
+
};
|
|
292
|
+
const p2 = {
|
|
293
|
+
status: state.status,
|
|
294
|
+
transaction_id: transactionId
|
|
295
|
+
};
|
|
296
|
+
const provider = {
|
|
297
|
+
...previous,
|
|
298
|
+
...p1,
|
|
299
|
+
...p2
|
|
300
|
+
};
|
|
301
|
+
const reference = transactionId ?? state.order_token;
|
|
302
|
+
const payment = {
|
|
303
|
+
provider_transaction_id: transactionId,
|
|
304
|
+
reference
|
|
305
|
+
};
|
|
306
|
+
const metadata = {
|
|
307
|
+
attempt_reference: ctx.attemptReference,
|
|
308
|
+
payment,
|
|
309
|
+
provider
|
|
310
|
+
};
|
|
311
|
+
const amount = base?.amount ?? ctx.amount.value;
|
|
312
|
+
const details = detailsOf(charge, reference);
|
|
313
|
+
if (mapped.status !== "FAILED") return {
|
|
314
|
+
status: mapped.status,
|
|
315
|
+
amount,
|
|
316
|
+
metadata,
|
|
317
|
+
details
|
|
318
|
+
};
|
|
319
|
+
const fallback = mapped.error ?? "UNKNOWN";
|
|
320
|
+
const error = deunaStateError({
|
|
321
|
+
method: charge.method,
|
|
322
|
+
state,
|
|
323
|
+
fallback,
|
|
324
|
+
attempt: ctx.attempt
|
|
325
|
+
});
|
|
326
|
+
if (fallback === "AMBIGUOUS") metadata.needs_review = true;
|
|
327
|
+
metadata.error = error;
|
|
328
|
+
return {
|
|
329
|
+
status: "FAILED",
|
|
330
|
+
amount,
|
|
331
|
+
metadata,
|
|
332
|
+
error,
|
|
333
|
+
details
|
|
334
|
+
};
|
|
335
|
+
};
|
|
336
|
+
const failedResult = (pending, error) => {
|
|
337
|
+
const metadata = {
|
|
338
|
+
...pending.metadata,
|
|
339
|
+
error
|
|
340
|
+
};
|
|
341
|
+
return {
|
|
342
|
+
...pending,
|
|
343
|
+
status: "FAILED",
|
|
344
|
+
metadata,
|
|
345
|
+
error
|
|
346
|
+
};
|
|
347
|
+
};
|
|
348
|
+
/** Order token of the attempt: result metadata, else the device storage. */
|
|
349
|
+
const orderTokenOf = async (ctx, result) => {
|
|
350
|
+
const token = record(result.metadata.provider).order_token;
|
|
351
|
+
if (typeof token === "string" && token) return token;
|
|
352
|
+
return ctx.storage.get(orderTokenKey(ctx.attemptReference));
|
|
353
|
+
};
|
|
354
|
+
/**
|
|
355
|
+
* DEUNA Payment Widget for every method (card, Nequi, Mercado Pago, Deuna
|
|
356
|
+
* App Wallet, PSE). Spark tokenizes the DEUNA order with the private key
|
|
357
|
+
* (`api.tokenize`), the template opens the widget with the public key, and
|
|
358
|
+
* the order status read back from Spark (`api.order`) is the one that
|
|
359
|
+
* counts. Refunds go through the DEUNA Admin (manual reversal).
|
|
360
|
+
*/
|
|
361
|
+
const deunaWidgetPayments = (options) => {
|
|
362
|
+
const { method, bridge, api } = options;
|
|
363
|
+
const language = options.language ?? "es";
|
|
364
|
+
const chargeOf = (ctx) => {
|
|
365
|
+
return {
|
|
366
|
+
method,
|
|
367
|
+
processor: resolveDeunaProcessor(method, ctx.config)
|
|
368
|
+
};
|
|
369
|
+
};
|
|
370
|
+
const orderState = (ctx, orderToken) => {
|
|
371
|
+
return api.order({
|
|
372
|
+
paymentId: ctx.paymentId,
|
|
373
|
+
orderToken
|
|
374
|
+
});
|
|
375
|
+
};
|
|
376
|
+
/** Spark state after the widget closed; `null` if it can't be read. */
|
|
377
|
+
const confirmed = async (ctx, charge, pending, orderToken) => {
|
|
378
|
+
const state = await orderState(ctx, orderToken).catch(() => null);
|
|
379
|
+
if (!state) return null;
|
|
380
|
+
return toResult(ctx, charge, state, pending);
|
|
381
|
+
};
|
|
382
|
+
const settle = async (ctx, charge, outcome, orderToken) => {
|
|
383
|
+
const pending = toResult(ctx, charge, {
|
|
384
|
+
order_token: orderToken,
|
|
385
|
+
status: "pending"
|
|
386
|
+
});
|
|
387
|
+
const attempt = ctx.attempt;
|
|
388
|
+
if (outcome.type === "pending") return pending;
|
|
389
|
+
if (outcome.type === "expired") {
|
|
390
|
+
const error = createPaymentError("WALLET_EXPIRED", { attempt });
|
|
391
|
+
return failedResult(pending, error);
|
|
392
|
+
}
|
|
393
|
+
if (outcome.type === "error") {
|
|
394
|
+
const raw = {
|
|
395
|
+
code: outcome.code ?? outcome.errorType ?? null,
|
|
396
|
+
message: outcome.message ?? null,
|
|
397
|
+
attempt,
|
|
398
|
+
fallback: deunaDeclinedCode(charge.method)
|
|
399
|
+
};
|
|
400
|
+
return failedResult(pending, toPaymentError(deunaErrors, raw));
|
|
401
|
+
}
|
|
402
|
+
const result = await confirmed(ctx, charge, pending, orderToken);
|
|
403
|
+
if (outcome.type === "success") return result ?? pending;
|
|
404
|
+
if (result && result.status !== "PENDING") return result;
|
|
405
|
+
if (outcome.by === "system") return result ?? pending;
|
|
406
|
+
const error = createPaymentError("CANCELLED_BY_USER", { attempt });
|
|
407
|
+
return failedResult(result ?? pending, error);
|
|
408
|
+
};
|
|
409
|
+
const start = async (ctx) => {
|
|
410
|
+
const publicApiKey = publicKeyOf(ctx.config);
|
|
411
|
+
const charge = chargeOf(ctx);
|
|
412
|
+
const t1 = {
|
|
413
|
+
paymentId: ctx.paymentId,
|
|
414
|
+
orderId: ctx.order.id
|
|
415
|
+
};
|
|
416
|
+
const t2 = { attemptReference: ctx.attemptReference };
|
|
417
|
+
const t3 = options.callbackUrls ? { callbackUrls: options.callbackUrls } : {};
|
|
418
|
+
const orderToken = (await api.tokenize({
|
|
419
|
+
...t1,
|
|
420
|
+
...t2,
|
|
421
|
+
...t3
|
|
422
|
+
})).order_token;
|
|
423
|
+
await ctx.storage.set(orderTokenKey(ctx.attemptReference), orderToken);
|
|
424
|
+
const paymentMethods = deunaWidgetPaymentMethods(charge.processor);
|
|
425
|
+
const o1 = {
|
|
426
|
+
publicApiKey,
|
|
427
|
+
environment: ctx.environment,
|
|
428
|
+
orderToken
|
|
429
|
+
};
|
|
430
|
+
const o2 = {
|
|
431
|
+
paymentMethods,
|
|
432
|
+
language,
|
|
433
|
+
signal: ctx.signal
|
|
434
|
+
};
|
|
435
|
+
const outcome = await bridge.open({
|
|
436
|
+
...o1,
|
|
437
|
+
...o2
|
|
438
|
+
});
|
|
439
|
+
return settle(ctx, charge, outcome, orderToken);
|
|
440
|
+
};
|
|
441
|
+
const query = async (ctx, result) => {
|
|
442
|
+
const orderToken = await orderTokenOf(ctx, result);
|
|
443
|
+
if (!orderToken) throw fail("AMBIGUOUS", "No DEUNA order for this attempt");
|
|
444
|
+
const state = await orderState(ctx, orderToken);
|
|
445
|
+
return toResult(ctx, chargeOf(ctx), state, result);
|
|
446
|
+
};
|
|
447
|
+
/** The widget order is left to expire; nothing was charged. */
|
|
448
|
+
const cancel = async (_ctx, result) => {
|
|
449
|
+
return {
|
|
450
|
+
...result,
|
|
451
|
+
status: "CANCELLED"
|
|
452
|
+
};
|
|
453
|
+
};
|
|
454
|
+
return {
|
|
455
|
+
provider: DEUNA_PROVIDER,
|
|
456
|
+
method,
|
|
457
|
+
errors: deunaErrors,
|
|
458
|
+
policy: DEUNA_WIDGET_POLICY,
|
|
459
|
+
start,
|
|
460
|
+
query,
|
|
461
|
+
cancel
|
|
462
|
+
};
|
|
463
|
+
};
|
|
464
|
+
//#endregion
|
|
465
|
+
export { DEUNA_PROCESSORS, DEUNA_PROCESSOR_KEYS, DEUNA_PROVIDER, DEUNA_WIDGET_POLICY, deunaDeclinedCode, deunaErrors, deunaProcessorsFor, deunaStateError, deunaWidgetPaymentMethods, deunaWidgetPayments, mapDeunaStatus, orderTokenKey, resolveDeunaProcessor };
|
package/package.json
CHANGED
|
@@ -1,6 +1,59 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fire-spark/payments-deuna-widget",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
3
|
+
"version": "0.11.0",
|
|
4
|
+
"description": "Fire Spark DEUNA Payment Widget adapter: cards, Nequi, Mercado Pago and Deuna App Wallet, and PSE through the DEUNA checkout",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"sideEffects": false,
|
|
7
|
+
"files": [
|
|
8
|
+
"dist"
|
|
9
|
+
],
|
|
10
|
+
"main": "./dist/index.js",
|
|
11
|
+
"types": "./dist/index.d.ts",
|
|
12
|
+
"exports": {
|
|
13
|
+
".": {
|
|
14
|
+
"types": "./dist/index.d.ts",
|
|
15
|
+
"import": "./dist/index.js",
|
|
16
|
+
"default": "./dist/index.js"
|
|
17
|
+
}
|
|
18
|
+
},
|
|
19
|
+
"dependencies": {
|
|
20
|
+
"@fire-spark/payments": "^0.11.0",
|
|
21
|
+
"@fire-spark/types": "^0.11.0"
|
|
22
|
+
},
|
|
23
|
+
"devDependencies": {
|
|
24
|
+
"tsdown": "^0.22.14",
|
|
25
|
+
"typescript": "^7.0.2",
|
|
26
|
+
"vitest": "^2.1.8"
|
|
27
|
+
},
|
|
28
|
+
"publishConfig": {
|
|
29
|
+
"access": "public"
|
|
30
|
+
},
|
|
31
|
+
"tsdown": {
|
|
32
|
+
"entry": [
|
|
33
|
+
"src/index.ts"
|
|
34
|
+
],
|
|
35
|
+
"format": [
|
|
36
|
+
"esm"
|
|
37
|
+
],
|
|
38
|
+
"platform": "neutral",
|
|
39
|
+
"dts": {
|
|
40
|
+
"tsconfig": "tsconfig.build.json"
|
|
41
|
+
},
|
|
42
|
+
"clean": true,
|
|
43
|
+
"deps": {
|
|
44
|
+
"neverBundle": [
|
|
45
|
+
"@fire-spark/payments",
|
|
46
|
+
"@fire-spark/types"
|
|
47
|
+
]
|
|
48
|
+
}
|
|
49
|
+
},
|
|
50
|
+
"scripts": {
|
|
51
|
+
"build": "tsdown",
|
|
52
|
+
"dev": "tsdown --watch",
|
|
53
|
+
"check-types": "tsc --noEmit",
|
|
54
|
+
"test:utils": "vitest run",
|
|
55
|
+
"lint": "oxlint ./src --max-warnings=0",
|
|
56
|
+
"format": "oxfmt ./src",
|
|
57
|
+
"check-format": "oxfmt --check ./src"
|
|
58
|
+
}
|
|
6
59
|
}
|
package/README.md
DELETED