@fire-spark/payments-redeban-pinpad 0.0.0-stage → 0.13.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.
@@ -0,0 +1,80 @@
1
+ import { DeviceField, PaymentAdapter, PaymentErrorEntry } from "@fire-spark/payments";
2
+ //#region src/parse.d.ts
3
+ type SipResponse = {
4
+ code: string;
5
+ message: string;
6
+ };
7
+ //#endregion
8
+ //#region src/client.d.ts
9
+ type TokenInput = {
10
+ baseUrl: string;
11
+ codigoUnico: string;
12
+ usuario: string;
13
+ clave: string;
14
+ };
15
+ type SendInput = {
16
+ baseUrl: string;
17
+ codigoUnico: string;
18
+ id: string;
19
+ data: string;
20
+ token: string;
21
+ };
22
+ type LookupInput = {
23
+ baseUrl: string;
24
+ codigoUnico: string;
25
+ id: string;
26
+ token: string;
27
+ };
28
+ type RedebanClient = {
29
+ token(input: TokenInput): Promise<SipResponse>;
30
+ send(input: SendInput): Promise<SipResponse>;
31
+ response(input: LookupInput): Promise<SipResponse>;
32
+ erase(input: LookupInput): Promise<SipResponse>;
33
+ };
34
+ /** REST client. Every call is POST with an empty body and query params. */
35
+ declare const createRedebanClient: (fetchImpl?: typeof fetch) => RedebanClient;
36
+ //#endregion
37
+ //#region src/errors.d.ts
38
+ /** Anexo 4. Keys are the technical `Cod`, not the financial response. */
39
+ declare const redebanPinPadErrors: Readonly<Record<string, PaymentErrorEntry>> & {
40
+ "03": PaymentErrorEntry;
41
+ "04": PaymentErrorEntry;
42
+ "05": PaymentErrorEntry;
43
+ "06": PaymentErrorEntry;
44
+ "07": PaymentErrorEntry;
45
+ "08": PaymentErrorEntry;
46
+ "09": PaymentErrorEntry;
47
+ "10": PaymentErrorEntry;
48
+ "11": PaymentErrorEntry;
49
+ "12": PaymentErrorEntry;
50
+ "14": PaymentErrorEntry;
51
+ "15": PaymentErrorEntry;
52
+ "16": PaymentErrorEntry;
53
+ "17": PaymentErrorEntry;
54
+ "18": PaymentErrorEntry;
55
+ "99": PaymentErrorEntry;
56
+ };
57
+ //#endregion
58
+ //#region src/settings.d.ts
59
+ declare const redebanPinPadDeviceFields: DeviceField[];
60
+ //#endregion
61
+ //#region src/poll.d.ts
62
+ type PollClock = {
63
+ now: () => number;
64
+ wait: (ms: number, signal: AbortSignal) => Promise<void>;
65
+ };
66
+ //#endregion
67
+ //#region src/adapter.d.ts
68
+ type RedebanPinPadOptions = {
69
+ client?: RedebanClient;
70
+ clock?: PollClock;
71
+ /** Console traces. Off unless this is true. */
72
+ debug?: boolean;
73
+ /** Backend info traces. Errors are sent either way. Default on. */
74
+ traces?: boolean;
75
+ /** Clock for the trace stamps. The poll clock stays separate. */
76
+ now?: () => Date;
77
+ };
78
+ declare const redebanPinPadPayments: (options?: RedebanPinPadOptions) => PaymentAdapter;
79
+ //#endregion
80
+ export { type RedebanClient, type RedebanPinPadOptions, createRedebanClient, redebanPinPadDeviceFields, redebanPinPadErrors, redebanPinPadPayments };
package/dist/index.js ADDED
@@ -0,0 +1,1189 @@
1
+ import { PaymentError, createPaymentError, definePaymentErrors, isPaymentError, toPaymentError } from "@fire-spark/payments";
2
+ import { log, logError } from "@fire-spark/core";
3
+ //#region src/constants.ts
4
+ /** First four polls, then a shorter gap. Matches the KIOSCOS client. */
5
+ const REDEBAN_POLL_INTERVALS_MS = [
6
+ 1e4,
7
+ 1e4,
8
+ 1e4,
9
+ 1e4
10
+ ];
11
+ /** Covers both poll phases so the orchestrator does not cut at 30s. */
12
+ const REDEBAN_TIMEOUT_MS = 37e4;
13
+ const REDEBAN_ERROR_SOURCE = "docs/payments/redeban/pinpad.md#11-errores";
14
+ //#endregion
15
+ //#region src/parse.ts
16
+ const CODE = /^Cod:\s*([^,]*)\s*,\s*Msj:\s*([\s\S]*)$/;
17
+ /** `Cod:00,Msj:...`. The message keeps its commas. */
18
+ const parseSipBody = (body) => {
19
+ const match = CODE.exec(body.trim());
20
+ if (!match) return null;
21
+ const code = match[1]?.trim() ?? "";
22
+ return {
23
+ code: /^\d{1,2}$/.test(code) ? code.padStart(2, "0") : code,
24
+ message: (match[2] ?? "").trim()
25
+ };
26
+ };
27
+ /** EnviarDatos success. A financial reply never starts with OK. */
28
+ const isTechnicalOk = (message) => {
29
+ return message.trim().toUpperCase().startsWith("OK");
30
+ };
31
+ /** Respuesta finished: Cod 00 and a financial message, not a bare OK. */
32
+ const isFinancialMessage = (message) => {
33
+ const text = message.trim();
34
+ if (!text) return false;
35
+ if (isTechnicalOk(text)) return false;
36
+ if (text.toUpperCase().startsWith("ERROR")) return false;
37
+ return true;
38
+ };
39
+ const at = (fields, index) => fields[index]?.trim() ?? "";
40
+ /** Anexo 2, in order, taken from the `Msj` of a finished Respuesta. */
41
+ const parseFinancial = (message) => {
42
+ const fields = message.split(",").map((field) => field.trim());
43
+ return {
44
+ responseCode: at(fields, 0),
45
+ approvalCode: at(fields, 1),
46
+ total: at(fields, 2),
47
+ iva: at(fields, 3),
48
+ receipt: at(fields, 4),
49
+ rrn: at(fields, 5),
50
+ terminal: at(fields, 6),
51
+ date: at(fields, 7),
52
+ time: at(fields, 8),
53
+ franchise: at(fields, 9),
54
+ accountType: at(fields, 10),
55
+ installments: at(fields, 11),
56
+ bin: at(fields, 12),
57
+ establishment: at(fields, 13),
58
+ address: at(fields, 14),
59
+ register: at(fields, 15),
60
+ cardName: at(fields, 16),
61
+ expiry: at(fields, 28),
62
+ fields
63
+ };
64
+ };
65
+ const isApprovedCode = (code) => {
66
+ return code === "00" || code === "0";
67
+ };
68
+ /** Pinpad receipt is 6 characters. */
69
+ const padReceipt = (value) => {
70
+ const clean = value.trim();
71
+ if (!clean) return "000000";
72
+ return clean.padStart(6, "0").slice(-6);
73
+ };
74
+ const integerField = (value) => {
75
+ const digits = value.replace(/\D/g, "");
76
+ if (!digits) return null;
77
+ return Number(digits);
78
+ };
79
+ /** Last four from a masked PAN, else from the BIN field. */
80
+ const lastFour = (financial) => {
81
+ const digits = (financial.fields.find((field) => field.includes("*")) || financial.bin || financial.cardName).replace(/\D/g, "");
82
+ if (digits.length < 4) return null;
83
+ return digits.slice(-4);
84
+ };
85
+ //#endregion
86
+ //#region src/client.ts
87
+ const joinUrl = (baseUrl, path) => {
88
+ const base = baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`;
89
+ return new URL(path.replace(/^\//, ""), base);
90
+ };
91
+ const post = async (fetchImpl, url) => {
92
+ const response = await fetchImpl(url, { method: "POST" });
93
+ const body = await response.text();
94
+ if (!response.ok) throw new Error(body || `SIPConnector HTTP ${response.status}`);
95
+ const parsed = parseSipBody(body);
96
+ if (!parsed) throw new Error("SIPConnector response has no Cod/Msj");
97
+ return parsed;
98
+ };
99
+ /** REST client. Every call is POST with an empty body and query params. */
100
+ const createRedebanClient = (fetchImpl = fetch) => {
101
+ return {
102
+ token: (input) => {
103
+ const url = joinUrl(input.baseUrl, "api/Token");
104
+ url.searchParams.set("CodigoUnico", input.codigoUnico);
105
+ url.searchParams.set("Usuario", input.usuario);
106
+ url.searchParams.set("Clave", input.clave);
107
+ return post(fetchImpl, url);
108
+ },
109
+ send: (input) => {
110
+ const url = joinUrl(input.baseUrl, "api/EnviarDatos");
111
+ url.searchParams.set("CodigoUnico", input.codigoUnico);
112
+ url.searchParams.set("IdTransaccion", input.id);
113
+ url.searchParams.set("Data", input.data);
114
+ url.searchParams.set("token", input.token);
115
+ url.searchParams.set("Sobreescribir", "S");
116
+ url.searchParams.set("Red", "0");
117
+ return post(fetchImpl, url);
118
+ },
119
+ response: (input) => {
120
+ const url = joinUrl(input.baseUrl, "api/Respuesta");
121
+ url.searchParams.set("CodigoUnico", input.codigoUnico);
122
+ url.searchParams.set("IdTransaccion", input.id);
123
+ url.searchParams.set("token", input.token);
124
+ url.searchParams.set("Red", "0");
125
+ return post(fetchImpl, url);
126
+ },
127
+ erase: (input) => {
128
+ const url = joinUrl(input.baseUrl, "api/Borrar");
129
+ url.searchParams.set("CodigoUnico", input.codigoUnico);
130
+ url.searchParams.set("IdTransaccion", input.id);
131
+ url.searchParams.set("Token", input.token);
132
+ url.searchParams.set("Red", "0");
133
+ return post(fetchImpl, url);
134
+ }
135
+ };
136
+ };
137
+ //#endregion
138
+ //#region src/errors.ts
139
+ const entry = (code, message, retryable = false) => {
140
+ return {
141
+ code,
142
+ message,
143
+ retryable,
144
+ source: REDEBAN_ERROR_SOURCE,
145
+ since: "1.5"
146
+ };
147
+ };
148
+ /** Anexo 4. Keys are the technical `Cod`, not the financial response. */
149
+ const redebanPinPadErrors = definePaymentErrors({
150
+ "03": entry("INVALID_REQUEST", "Incorrect parameters"),
151
+ "04": entry("INVALID_CREDENTIALS", "Token is not valid", true),
152
+ "05": entry("INVALID_REQUEST", "Transaction id is incorrect"),
153
+ "06": entry("TERMINAL_BUSY", "Terminal already has a transaction", true),
154
+ "07": entry("INVALID_CREDENTIALS", "Unique code is invalid"),
155
+ "08": entry("INVALID_REQUEST", "Incomplete data structure"),
156
+ "09": entry("INVALID_REQUEST", "Transaction id already exists"),
157
+ "10": entry("PROVIDER_UNAVAILABLE", "Could not insert the transaction", true),
158
+ "11": entry("PROVIDER_UNAVAILABLE", "Could not insert the transaction", true),
159
+ "12": entry("INVALID_REQUEST", "Structure or LRC error"),
160
+ "14": entry("PROVIDER_UNAVAILABLE", "Internal process error", true),
161
+ "15": entry("INVALID_CREDENTIALS", "Token data is invalid", true),
162
+ "16": entry("INVALID_CREDENTIALS", "Authentication failed"),
163
+ "17": entry("UNKNOWN", "Record to delete was not found"),
164
+ "18": entry("INVALID_REQUEST", "Transaction is already resolved"),
165
+ "99": entry("PROVIDER_UNAVAILABLE", "Technical error", true)
166
+ });
167
+ //#endregion
168
+ //#region src/settings.ts
169
+ const redebanPinPadDeviceFields = [
170
+ {
171
+ key: "terminal_id",
172
+ label: "Terminal code",
173
+ type: "text",
174
+ required: true
175
+ },
176
+ {
177
+ key: "register_number",
178
+ label: "Register number",
179
+ type: "text",
180
+ required: true
181
+ },
182
+ {
183
+ key: "cashier_code",
184
+ label: "Cashier code",
185
+ type: "text",
186
+ required: true
187
+ }
188
+ ];
189
+ const text = (value) => {
190
+ return typeof value === "string" ? value.trim() : "";
191
+ };
192
+ const clip$1 = (value, max) => value.slice(0, max);
193
+ /** Merchant keys live on `config`. Terminal keys live on the device. */
194
+ const readRedebanSettings = (config, device) => {
195
+ const baseUrl = text(config.base_url);
196
+ const codigoUnico = text(config.codigo_unico);
197
+ const usuario = text(config.usuario);
198
+ const clave = text(config.secret_clave);
199
+ const supervisorPassword = text(config.secret_supervisor_password);
200
+ const terminalId = text(device.terminal_id);
201
+ const registerNumber = text(device.register_number);
202
+ const cashierCode = text(device.cashier_code);
203
+ const missing = [
204
+ ["base_url", baseUrl],
205
+ ["codigo_unico", codigoUnico],
206
+ ["usuario", usuario],
207
+ ["secret_clave", clave],
208
+ ["secret_supervisor_password", supervisorPassword],
209
+ ["terminal_id", terminalId],
210
+ ["register_number", registerNumber],
211
+ ["cashier_code", cashierCode]
212
+ ].filter(([, value]) => !value).map(([key]) => key);
213
+ if (missing.length) return {
214
+ ok: false,
215
+ missing
216
+ };
217
+ return {
218
+ ok: true,
219
+ settings: {
220
+ baseUrl,
221
+ codigoUnico,
222
+ usuario,
223
+ clave,
224
+ supervisorPassword,
225
+ terminalId: clip$1(terminalId, 10),
226
+ registerNumber: clip$1(registerNumber, 10),
227
+ cashierCode: clip$1(cashierCode, 10)
228
+ }
229
+ };
230
+ };
231
+ //#endregion
232
+ //#region src/trace.ts
233
+ /** One in-call trace. Discarded when the operation is not reported. */
234
+ const DETAIL_MAX = 2e3;
235
+ const MARK = "\n…\n";
236
+ /** 14–19 digits. Amounts, dates and short ids stay as-is. */
237
+ const PAN = /\d{14,19}/g;
238
+ /** Track 1/2 sentinels. The payload is not a field we record on purpose. */
239
+ const TRACK = /[%;][\dA-Za-z=^]{12,}/g;
240
+ /** Query values that can carry the service key, the token or the frame. */
241
+ const SECRET_QUERY = /([?&](?:Clave|token|Token|Data)=)[^&\s"')]+/gi;
242
+ const pad = (value, width) => {
243
+ return String(value).padStart(width, "0");
244
+ };
245
+ /** Local kiosk time of the step, not the time the log is posted. */
246
+ const redebanStamp = (date) => {
247
+ return `${pad(date.getHours(), 2)}:${pad(date.getMinutes(), 2)}:${pad(date.getSeconds(), 2)}.${pad(date.getMilliseconds(), 3)}`;
248
+ };
249
+ /** PAN and track data only. Amounts and short ids stay. */
250
+ const maskCardData = (value) => {
251
+ return value.replace(TRACK, "<track>").replace(PAN, (digits) => {
252
+ return `************${digits.slice(-4)}`;
253
+ });
254
+ };
255
+ /** PAN, track data, and secrets that arrived inside a URL. */
256
+ const redactSecrets = (value) => {
257
+ return maskCardData(value).replace(SECRET_QUERY, "$1<redacted>");
258
+ };
259
+ /** Keeps the start and the end when `detail` would pass 2000 characters. */
260
+ const fitDetail = (text) => {
261
+ if (text.length <= DETAIL_MAX) return text;
262
+ const head = Math.ceil((DETAIL_MAX - 3) / 2);
263
+ return `${text.slice(0, head)}${MARK}${text.slice(-998)}`;
264
+ };
265
+ const createRedebanTrace = (now = () => /* @__PURE__ */ new Date()) => {
266
+ const lines = [];
267
+ const push = (body) => {
268
+ const stamp = redebanStamp(now());
269
+ lines.push(`${stamp} ${redactSecrets(body)}`);
270
+ };
271
+ return {
272
+ call: (body) => push(`→ ${body}`),
273
+ result: (body) => push(`← ${body}`),
274
+ note: (body) => push(body),
275
+ detail: () => fitDetail(lines.join("\n"))
276
+ };
277
+ };
278
+ //#endregion
279
+ //#region src/debug.ts
280
+ const TAG = "[REDEBAN_DEBUG]";
281
+ /** Keys whose values are a token, a password, or the raw SIP message. */
282
+ const HIDDEN = /* @__PURE__ */ new Set([
283
+ "message",
284
+ "token",
285
+ "clave",
286
+ "data",
287
+ "password",
288
+ "supervisorPassword",
289
+ "secret",
290
+ "secret_clave",
291
+ "secret_supervisor_password",
292
+ "Msj",
293
+ "Clave",
294
+ "Token",
295
+ "Data"
296
+ ]);
297
+ let enabled = false;
298
+ const elapsed = (startedAt) => Date.now() - startedAt;
299
+ const safe = (value) => {
300
+ if (typeof value === "string") return redactSecrets(value);
301
+ if (value instanceof Map) return safe(Object.fromEntries(value));
302
+ if (value instanceof Error) {
303
+ const code = value.code;
304
+ const message = redactSecrets(value.message);
305
+ return {
306
+ name: value.name,
307
+ message,
308
+ code
309
+ };
310
+ }
311
+ if (Array.isArray(value)) return value.map((item) => safe(item));
312
+ if (!value || typeof value !== "object") return value;
313
+ const out = {};
314
+ for (const [name, item] of Object.entries(value)) {
315
+ if (HIDDEN.has(name)) continue;
316
+ out[name] = safe(item);
317
+ }
318
+ return out;
319
+ };
320
+ /** Turns console traces on or off. Default is off. */
321
+ const setRedebanLogs = (value) => {
322
+ enabled = value;
323
+ };
324
+ /** Logs one step: `[REDEBAN_DEBUG] scope › event { data }`. */
325
+ const redebanDebug = (scope, event, data) => {
326
+ if (!enabled) return;
327
+ const time = (/* @__PURE__ */ new Date()).toISOString();
328
+ const line = `${scope} › ${event}`;
329
+ if (data === void 0) {
330
+ console.log(TAG, time, line);
331
+ return;
332
+ }
333
+ console.log(TAG, time, line, safe(data));
334
+ };
335
+ /** Logs the input, the output or the error, and the duration of a call. */
336
+ const redebanDebugCall = async (scope, input, call) => {
337
+ const startedAt = Date.now();
338
+ redebanDebug(scope, "→ in", input);
339
+ try {
340
+ const output = await call();
341
+ const ms = elapsed(startedAt);
342
+ redebanDebug(scope, "← out", {
343
+ output,
344
+ ms
345
+ });
346
+ return output;
347
+ } catch (error) {
348
+ const ms = elapsed(startedAt);
349
+ redebanDebug(scope, "✕ error", {
350
+ error,
351
+ ms
352
+ });
353
+ throw error;
354
+ }
355
+ };
356
+ //#endregion
357
+ //#region src/frame.ts
358
+ const clip = (value, max) => value.trim().slice(0, max);
359
+ /** Integer pesos. SIPConnector rejects separators and decimals. */
360
+ const pesos = (value) => {
361
+ if (!Number.isFinite(value)) return "0";
362
+ return String(Math.round(value));
363
+ };
364
+ const shared = (input) => {
365
+ const amount = pesos(input.amount);
366
+ const iva = pesos(input.iva);
367
+ const invoice = clip(input.invoice, 10);
368
+ const head = [
369
+ amount,
370
+ iva,
371
+ "0",
372
+ "0",
373
+ amount,
374
+ invoice
375
+ ];
376
+ const tail = [
377
+ clip(input.cashierCode, 10),
378
+ "0",
379
+ "0",
380
+ "0",
381
+ clip(input.terminalId, 10),
382
+ clip(input.registerNumber, 10)
383
+ ];
384
+ return {
385
+ amount,
386
+ iva,
387
+ invoice,
388
+ fields: [...head, ...tail]
389
+ };
390
+ };
391
+ /** Compra, Anexo 1.1. Twenty fields. Multicommerce repeats the unique code. */
392
+ const saleFrame = (input) => {
393
+ const { invoice, fields } = shared(input);
394
+ const codigo = clip(input.codigoUnico, 10);
395
+ const rest = [
396
+ "0",
397
+ "0",
398
+ invoice,
399
+ "3"
400
+ ];
401
+ const end = [
402
+ "N",
403
+ codigo,
404
+ " "
405
+ ];
406
+ return [
407
+ "0",
408
+ ...fields,
409
+ ...rest,
410
+ ...end
411
+ ].join(",");
412
+ };
413
+ /**
414
+ * Anulación total, Anexo 1.2. Eighteen fields.
415
+ * Without multicommerce the last field stays empty.
416
+ */
417
+ const voidFrame = (input) => {
418
+ const { fields } = shared(input);
419
+ const rest = [
420
+ padReceipt(input.receipt),
421
+ clip(input.supervisorPassword, 4),
422
+ "3"
423
+ ];
424
+ const end = ["N", ""];
425
+ return [
426
+ "1",
427
+ ...fields,
428
+ ...rest,
429
+ ...end
430
+ ].join(",");
431
+ };
432
+ //#endregion
433
+ //#region src/metadata.ts
434
+ const ALPHABET = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
435
+ /** SIPConnector ids are 1 to 10 alphanumeric characters. */
436
+ const transactionId = (attemptReference) => {
437
+ const clean = attemptReference.replace(/[^0-9A-Za-z]/g, "");
438
+ if (clean.length >= 1 && clean.length <= 10) return clean;
439
+ let hash = 2166136261;
440
+ for (let i = 0; i < attemptReference.length; i += 1) {
441
+ hash ^= attemptReference.charCodeAt(i);
442
+ hash = Math.imul(hash, 16777619);
443
+ }
444
+ let value = hash >>> 0;
445
+ let out = "";
446
+ for (let i = 0; i < 10; i += 1) {
447
+ out += ALPHABET[value % 62] ?? "0";
448
+ value = Math.floor(value / 62);
449
+ value += hash >>> i % 16;
450
+ }
451
+ return out.slice(0, 10);
452
+ };
453
+ const ivaOf = (taxes) => {
454
+ const sum = taxes.reduce((total, tax) => total + tax.tax, 0);
455
+ return Number(pesos(sum));
456
+ };
457
+ const cardTypeOf = (account) => {
458
+ if (account === "CR") return "CREDIT";
459
+ if (account === "AH" || account === "CC") return "DEBIT";
460
+ return null;
461
+ };
462
+ const providerOf = (snapshot, financial) => {
463
+ const base = {
464
+ id_transaccion: snapshot.id,
465
+ valor_venta: snapshot.amount,
466
+ valor_iva: snapshot.iva,
467
+ valor_total: snapshot.total,
468
+ numero_recibo: snapshot.receipt
469
+ };
470
+ if (!financial) return base;
471
+ return {
472
+ ...base,
473
+ codigo_respuesta: financial.responseCode,
474
+ codigo_aprobacion: financial.approvalCode,
475
+ rrn: financial.rrn,
476
+ numero_recibo: financial.receipt || snapshot.receipt,
477
+ raw_message: financial.fields.join(",")
478
+ };
479
+ };
480
+ const paymentOf = (financial) => {
481
+ const count = integerField(financial.installments) ?? 0;
482
+ const installments = count > 0 ? {
483
+ count,
484
+ type: null
485
+ } : null;
486
+ const date = financial.date ? `20${financial.date}` : null;
487
+ return {
488
+ provider_transaction_id: financial.approvalCode || null,
489
+ authorization_code: financial.approvalCode || null,
490
+ reference: financial.rrn || null,
491
+ card_type: cardTypeOf(financial.accountType),
492
+ brand: financial.franchise || null,
493
+ last4: lastFour(financial),
494
+ installments,
495
+ terminal_id: financial.terminal || null,
496
+ merchant_id: financial.establishment || null,
497
+ ticket_number: financial.receipt || null,
498
+ transaction_date: date,
499
+ account_type: financial.accountType || null
500
+ };
501
+ };
502
+ const snapshotOf = (result) => {
503
+ const provider = result.metadata.provider;
504
+ if (!provider) return null;
505
+ const id = provider.id_transaccion;
506
+ const amount = provider.valor_venta;
507
+ const iva = provider.valor_iva;
508
+ const total = provider.valor_total;
509
+ const receipt = provider.numero_recibo;
510
+ if (typeof id !== "string" || !id) return null;
511
+ if (typeof amount !== "number" || typeof iva !== "number") return null;
512
+ if (typeof total !== "number") return null;
513
+ return {
514
+ id,
515
+ amount,
516
+ iva,
517
+ total,
518
+ receipt: typeof receipt === "string" ? receipt : ""
519
+ };
520
+ };
521
+ const metadataOf = (attemptReference, snapshot, financial) => {
522
+ const provider = providerOf(snapshot, financial);
523
+ return {
524
+ attempt_reference: attemptReference,
525
+ payment: financial ? paymentOf(financial) : void 0,
526
+ provider
527
+ };
528
+ };
529
+ //#endregion
530
+ //#region src/poll.ts
531
+ const defaultPollClock = () => {
532
+ return {
533
+ now: () => Date.now(),
534
+ wait: (ms, signal) => {
535
+ return new Promise((resolve) => {
536
+ if (signal.aborted) {
537
+ resolve();
538
+ return;
539
+ }
540
+ const onAbort = () => {
541
+ clearTimeout(timer);
542
+ resolve();
543
+ };
544
+ const timer = setTimeout(() => {
545
+ signal.removeEventListener("abort", onAbort);
546
+ resolve();
547
+ }, ms);
548
+ signal.addEventListener("abort", onAbort, { once: true });
549
+ });
550
+ }
551
+ };
552
+ };
553
+ const intervalAt = (index) => {
554
+ return REDEBAN_POLL_INTERVALS_MS[index] ?? 5e3;
555
+ };
556
+ /**
557
+ * Polls Respuesta. Cod 01 or 02 refreshes the token and resets the
558
+ * deadline once. Cod 02 ignores abort: the card is already in the pinpad.
559
+ */
560
+ const pollRespuesta = async (input) => {
561
+ const { client, settings, id, signal, clock, refreshToken, trace } = input;
562
+ let token = input.token;
563
+ let start = clock.now();
564
+ let reset = false;
565
+ let last = "";
566
+ let seen = "";
567
+ let polls = 0;
568
+ const lookup = () => {
569
+ const baseUrl = settings.baseUrl;
570
+ const codigoUnico = settings.codigoUnico;
571
+ return client.response({
572
+ baseUrl,
573
+ codigoUnico,
574
+ id,
575
+ token
576
+ });
577
+ };
578
+ while (true) {
579
+ const waiting = last === "" || last === "00" || last === "01";
580
+ if (signal.aborted && waiting) return {
581
+ kind: "cancelled",
582
+ token
583
+ };
584
+ const response = await redebanDebugCall("redeban.poll", {
585
+ id,
586
+ index: polls
587
+ }, async () => {
588
+ const body = await lookup();
589
+ const done = body.code === "00" && isFinancialMessage(body.message);
590
+ const length = body.message.length;
591
+ return {
592
+ ...body,
593
+ financial: done,
594
+ length
595
+ };
596
+ });
597
+ if (response.code !== seen) {
598
+ trace?.result(`respuesta ${response.code}`);
599
+ seen = response.code;
600
+ }
601
+ last = response.code;
602
+ if (response.code === "04") {
603
+ trace?.note("token refresh");
604
+ token = await refreshToken(true);
605
+ continue;
606
+ }
607
+ if (response.code === "01" || response.code === "02") {
608
+ token = await refreshToken(false);
609
+ if (!reset) {
610
+ reset = true;
611
+ start = clock.now();
612
+ }
613
+ } else if (response.code === "00" && isFinancialMessage(response.message)) return {
614
+ kind: "done",
615
+ message: response.message,
616
+ token
617
+ };
618
+ else if (response.code !== "00") return {
619
+ kind: "technical",
620
+ response,
621
+ token
622
+ };
623
+ if (clock.now() - start >= 184e3) return {
624
+ kind: "timeout",
625
+ started: reset,
626
+ token,
627
+ last
628
+ };
629
+ await clock.wait(intervalAt(polls), signal);
630
+ polls += 1;
631
+ }
632
+ };
633
+ //#endregion
634
+ //#region src/report.ts
635
+ /** Reports one pinpad step to the merchant dashboard. */
636
+ const CHARGE_DID_NOT_START = "Redeban charge did not start";
637
+ const CHARGE_DECLINED = "Redeban charge declined";
638
+ const CHARGE_DID_NOT_FINISH = "Redeban charge did not finish";
639
+ const VOID_DECLINED = "Redeban void declined";
640
+ const VOID_DID_NOT_FINISH = "Redeban void did not finish";
641
+ const redactedCause = (error) => {
642
+ if (error instanceof Error) {
643
+ const cause = new Error(redactSecrets(error.message));
644
+ cause.name = error.name;
645
+ if (error.stack) cause.stack = redactSecrets(error.stack);
646
+ return cause;
647
+ }
648
+ if (typeof error === "string") return redactSecrets(error);
649
+ return error;
650
+ };
651
+ /** Fixed phrase as the message. The thrown value keeps its name and stack. */
652
+ const reportedError = (message, error) => {
653
+ const wrapped = new Error(message);
654
+ const cause = redactedCause(error);
655
+ if (cause instanceof Error) {
656
+ wrapped.name = cause.name;
657
+ if (cause.stack) wrapped.stack = cause.stack;
658
+ }
659
+ wrapped.cause = cause;
660
+ return wrapped;
661
+ };
662
+ /** One backend log. `info` is skipped when `traces` is off. Never throws. */
663
+ const reportRedeban = (input) => {
664
+ if (input.level === "info" && !input.traces) return;
665
+ const options = {
666
+ alias: input.alias,
667
+ status: input.status,
668
+ detail: input.detail
669
+ };
670
+ if (!(input.level === "error" && input.error != null)) {
671
+ log(input.level, input.message, options);
672
+ return;
673
+ }
674
+ logError(reportedError(input.message, input.error), options);
675
+ };
676
+ const statusOf = (result) => {
677
+ const code = result.error?.code;
678
+ if (!code) return "failed";
679
+ return code.toLowerCase();
680
+ };
681
+ const send = (input, level, alias, status, message) => {
682
+ const thrown = level === "error" ? input.thrown : void 0;
683
+ reportRedeban({
684
+ level,
685
+ alias,
686
+ status,
687
+ message,
688
+ detail: input.trace.detail(),
689
+ traces: input.traces,
690
+ error: thrown
691
+ });
692
+ };
693
+ const reportCharge = (input) => {
694
+ const { result, opened } = input;
695
+ if (result.status === "PAID") return;
696
+ const cancelled = result.status === "CANCELLED";
697
+ const declined = result.error?.code === "CARD_DECLINED";
698
+ if (cancelled || declined) {
699
+ send(input, "info", "redeban.charge", "declined", CHARGE_DECLINED);
700
+ return;
701
+ }
702
+ const message = opened ? CHARGE_DID_NOT_FINISH : CHARGE_DID_NOT_START;
703
+ send(input, "error", "redeban.charge", statusOf(result), message);
704
+ };
705
+ const reportQuery = (input) => {
706
+ const { result } = input;
707
+ if (result.status === "PAID") return;
708
+ if (result.error?.code === "AMBIGUOUS") return;
709
+ if (result.error?.code === "CARD_DECLINED") {
710
+ send(input, "info", "redeban.charge", "declined", CHARGE_DECLINED);
711
+ return;
712
+ }
713
+ const status = statusOf(result);
714
+ send(input, "error", "redeban.charge", status, CHARGE_DID_NOT_FINISH);
715
+ };
716
+ const reportVoid = (input) => {
717
+ const { result, wasPaid } = input;
718
+ if (result.status === "CANCELLED") return;
719
+ if (wasPaid && result.error?.code === "CARD_DECLINED") {
720
+ send(input, "info", "redeban.void", "declined", VOID_DECLINED);
721
+ return;
722
+ }
723
+ const status = statusOf(result);
724
+ send(input, "error", "redeban.void", status, VOID_DID_NOT_FINISH);
725
+ };
726
+ /** Decides the single backend log for one adapter call. Success stays quiet. */
727
+ const reportOperation = (input) => {
728
+ if (input.kind === "charge") {
729
+ reportCharge(input);
730
+ return;
731
+ }
732
+ if (input.kind === "query") {
733
+ reportQuery(input);
734
+ return;
735
+ }
736
+ reportVoid(input);
737
+ };
738
+ //#endregion
739
+ //#region src/adapter.ts
740
+ const baseResult = (ctx, amount, snapshot, extra = {}) => {
741
+ const reference = ctx.attemptReference;
742
+ return {
743
+ status: "FAILED",
744
+ amount,
745
+ metadata: snapshot ? metadataOf(reference, snapshot) : { attempt_reference: reference },
746
+ ...extra
747
+ };
748
+ };
749
+ const withError = (ctx, amount, snapshot, error, needsReview = false) => {
750
+ const result = baseResult(ctx, amount, snapshot);
751
+ const review = needsReview ? { needs_review: true } : {};
752
+ const metadata = {
753
+ ...result.metadata,
754
+ error,
755
+ ...review
756
+ };
757
+ return {
758
+ ...result,
759
+ metadata,
760
+ error
761
+ };
762
+ };
763
+ const technicalError = (ctx, response) => {
764
+ return toPaymentError(redebanPinPadErrors, {
765
+ code: response.code,
766
+ message: response.message,
767
+ attempt: ctx.attempt,
768
+ fallback: "UNKNOWN"
769
+ });
770
+ };
771
+ const loggedSettings = (settings) => {
772
+ return {
773
+ baseUrl: settings.baseUrl,
774
+ codigoUnico: settings.codigoUnico,
775
+ usuario: settings.usuario,
776
+ terminalId: settings.terminalId,
777
+ registerNumber: settings.registerNumber,
778
+ cashierCode: settings.cashierCode
779
+ };
780
+ };
781
+ const issueToken = async (client, settings, ctx, trace) => {
782
+ const baseUrl = settings.baseUrl;
783
+ const codigoUnico = settings.codigoUnico;
784
+ const usuario = settings.usuario;
785
+ const clave = settings.clave;
786
+ const shown = {
787
+ codigoUnico,
788
+ usuario
789
+ };
790
+ trace?.call("token");
791
+ try {
792
+ const response = await redebanDebugCall("redeban.token", shown, () => {
793
+ return client.token({
794
+ baseUrl,
795
+ codigoUnico,
796
+ usuario,
797
+ clave
798
+ });
799
+ });
800
+ if (response.code === "00" && response.message) {
801
+ trace?.result("token 00");
802
+ return {
803
+ ok: true,
804
+ token: response.message
805
+ };
806
+ }
807
+ trace?.result(`token ${response.code}`);
808
+ return {
809
+ ok: false,
810
+ error: technicalError(ctx, response)
811
+ };
812
+ } catch (error) {
813
+ trace?.result("token error");
814
+ throw error;
815
+ }
816
+ };
817
+ const erase = async (client, settings, id, token, trace) => {
818
+ const baseUrl = settings.baseUrl;
819
+ const codigoUnico = settings.codigoUnico;
820
+ trace.call(`borrar id=${id}`);
821
+ try {
822
+ const response = await redebanDebugCall("redeban.erase", { id }, () => {
823
+ return client.erase({
824
+ baseUrl,
825
+ codigoUnico,
826
+ id,
827
+ token
828
+ });
829
+ });
830
+ trace.result(`borrar ${response.code}`);
831
+ } catch {
832
+ trace.result("borrar error");
833
+ }
834
+ };
835
+ const noteFinancial = (trace, message) => {
836
+ const financial = parseFinancial(message);
837
+ const code = `code=${financial.responseCode}`;
838
+ const total = `total=${financial.total}`;
839
+ const receipt = `receipt=${financial.receipt}`;
840
+ const rrn = `rrn=${financial.rrn}`;
841
+ trace.note(`financial ${code} ${total} ${receipt} ${rrn}`);
842
+ return financial;
843
+ };
844
+ const finishFinancial = (ctx, amount, snapshot, message, trace) => {
845
+ const financial = noteFinancial(trace, message);
846
+ const next = {
847
+ ...snapshot,
848
+ receipt: financial.receipt || snapshot.receipt
849
+ };
850
+ const metadata = metadataOf(ctx.attemptReference, next, financial);
851
+ if (!isApprovedCode(financial.responseCode)) {
852
+ const error = toPaymentError(redebanPinPadErrors, {
853
+ code: financial.responseCode,
854
+ message: "Card declined",
855
+ attempt: ctx.attempt,
856
+ fallback: "CARD_DECLINED"
857
+ });
858
+ return {
859
+ status: "FAILED",
860
+ amount,
861
+ metadata: {
862
+ ...metadata,
863
+ error
864
+ },
865
+ error
866
+ };
867
+ }
868
+ const approved = integerField(financial.total);
869
+ if (approved !== amount) {
870
+ trace.note(`amount order=${amount} approved=${approved}`);
871
+ const error = createPaymentError("INVALID_REQUEST", {
872
+ message: "Approved amount does not match the order",
873
+ attempt: ctx.attempt
874
+ });
875
+ return {
876
+ status: "FAILED",
877
+ amount,
878
+ metadata: {
879
+ ...metadata,
880
+ error,
881
+ needs_review: true
882
+ },
883
+ error
884
+ };
885
+ }
886
+ const payment = metadata.payment;
887
+ return {
888
+ status: "PAID",
889
+ amount,
890
+ metadata: {
891
+ ...metadata,
892
+ payment
893
+ }
894
+ };
895
+ };
896
+ const finishPoll = async (ctx, client, settings, amount, snapshot, polled, trace) => {
897
+ if (polled.kind === "cancelled") {
898
+ await erase(client, settings, snapshot.id, polled.token, trace);
899
+ const error = createPaymentError("CANCELLED_BY_USER", { attempt: ctx.attempt });
900
+ return {
901
+ ...withError(ctx, amount, snapshot, error),
902
+ status: "CANCELLED"
903
+ };
904
+ }
905
+ if (polled.kind === "timeout") {
906
+ const started = polled.started ? "yes" : "no";
907
+ trace.note(`timeout started=${started} last=${polled.last}`);
908
+ if (!polled.started) await erase(client, settings, snapshot.id, polled.token, trace);
909
+ const error = createPaymentError(polled.started ? "AMBIGUOUS" : "TIMEOUT", { attempt: ctx.attempt });
910
+ const result = withError(ctx, amount, snapshot, error, polled.started);
911
+ const provider = {
912
+ ...result.metadata.provider,
913
+ last_code: polled.last
914
+ };
915
+ const metadata = {
916
+ ...result.metadata,
917
+ provider
918
+ };
919
+ return {
920
+ ...result,
921
+ metadata
922
+ };
923
+ }
924
+ if (polled.kind === "technical") {
925
+ const error = technicalError(ctx, polled.response);
926
+ return withError(ctx, amount, snapshot, error);
927
+ }
928
+ return finishFinancial(ctx, amount, snapshot, polled.message, trace);
929
+ };
930
+ const redebanPinPadPayments = (options = {}) => {
931
+ setRedebanLogs(options.debug === true);
932
+ const traces = options.traces !== false;
933
+ const now = options.now ?? (() => /* @__PURE__ */ new Date());
934
+ const client = options.client ?? createRedebanClient();
935
+ const clock = options.clock ?? defaultPollClock();
936
+ const refresh = async (settings, ctx, trace) => {
937
+ const issued = await issueToken(client, settings, ctx, trace);
938
+ if (!issued.ok) throw new PaymentError(issued.error);
939
+ return issued.token;
940
+ };
941
+ const poll = (ctx, settings, id, token, trace) => {
942
+ const signal = ctx.signal;
943
+ const refreshToken = (record) => {
944
+ return refresh(settings, ctx, record ? trace : void 0);
945
+ };
946
+ return pollRespuesta({
947
+ client,
948
+ settings,
949
+ id,
950
+ token,
951
+ signal,
952
+ clock,
953
+ refreshToken,
954
+ trace
955
+ });
956
+ };
957
+ const runLogged = async (scope, ctx, amount, snapshot, trace, kind, opened, wasPaid, run) => {
958
+ let thrown;
959
+ let result;
960
+ try {
961
+ result = await run();
962
+ } catch (error) {
963
+ thrown = error;
964
+ trace.result("error");
965
+ if (isPaymentError(error)) result = withError(ctx, amount, snapshot, error.error);
966
+ else {
967
+ const network = createPaymentError("NETWORK_ERROR", {
968
+ message: error instanceof Error ? error.message : "Network error",
969
+ attempt: ctx.attempt
970
+ });
971
+ result = withError(ctx, amount, snapshot, network);
972
+ }
973
+ }
974
+ const code = result.error?.code ?? null;
975
+ redebanDebug(scope, "outcome", {
976
+ status: result.status,
977
+ code
978
+ });
979
+ reportOperation({
980
+ kind,
981
+ traces,
982
+ trace,
983
+ result,
984
+ opened: opened(),
985
+ wasPaid,
986
+ thrown
987
+ });
988
+ return result;
989
+ };
990
+ const readSettings = (ctx, trace) => {
991
+ const read = readRedebanSettings(ctx.config, ctx.deviceSettings);
992
+ if (!read.ok) {
993
+ const message = read.missing.join(", ");
994
+ trace.note(`missing ${message}`);
995
+ redebanDebug("redeban.settings", "missing", { missing: read.missing });
996
+ return {
997
+ ok: false,
998
+ error: createPaymentError("TERMINAL_NOT_CONFIGURED", {
999
+ message,
1000
+ attempt: ctx.attempt
1001
+ })
1002
+ };
1003
+ }
1004
+ redebanDebug("redeban.settings", "read", loggedSettings(read.settings));
1005
+ return {
1006
+ ok: true,
1007
+ settings: read.settings
1008
+ };
1009
+ };
1010
+ const start = (ctx) => {
1011
+ const amount = Math.round(ctx.amount.value);
1012
+ const trace = createRedebanTrace(now);
1013
+ const state = { opened: false };
1014
+ trace.note(`reference=${ctx.attemptReference}`);
1015
+ return runLogged("redeban.start", ctx, amount, null, trace, "charge", () => state.opened, false, async () => {
1016
+ const read = readSettings(ctx, trace);
1017
+ if (!read.ok) return withError(ctx, amount, null, read.error);
1018
+ const { settings } = read;
1019
+ ctx.ui.message({
1020
+ text: "Present the card on the pinpad",
1021
+ kind: "waiting"
1022
+ });
1023
+ const issued = await issueToken(client, settings, ctx, trace);
1024
+ if (!issued.ok) return withError(ctx, amount, null, issued.error);
1025
+ const id = transactionId(ctx.attemptReference);
1026
+ const iva = ivaOf(ctx.taxes);
1027
+ const invoice = id;
1028
+ const cashierCode = settings.cashierCode;
1029
+ const terminalId = settings.terminalId;
1030
+ const registerNumber = settings.registerNumber;
1031
+ const codigoUnico = settings.codigoUnico;
1032
+ const frame = {
1033
+ amount,
1034
+ iva,
1035
+ invoice,
1036
+ cashierCode,
1037
+ terminalId,
1038
+ registerNumber,
1039
+ codigoUnico
1040
+ };
1041
+ const snapshot = {
1042
+ id,
1043
+ amount,
1044
+ iva,
1045
+ total: amount,
1046
+ receipt: id
1047
+ };
1048
+ const reference = ctx.attemptReference;
1049
+ const sale = `id=${id} amount=${amount}`;
1050
+ const who = `reference=${reference} terminal=${terminalId}`;
1051
+ trace.call(`enviar ${sale} ${who}`);
1052
+ const sent = await redebanDebugCall("redeban.send", { id }, () => {
1053
+ return client.send({
1054
+ baseUrl: settings.baseUrl,
1055
+ codigoUnico,
1056
+ id,
1057
+ data: saleFrame(frame),
1058
+ token: issued.token
1059
+ });
1060
+ });
1061
+ trace.result(`enviar ${sent.code}`);
1062
+ if (sent.code !== "00") return withError(ctx, amount, snapshot, technicalError(ctx, sent));
1063
+ state.opened = true;
1064
+ const polled = await poll(ctx, settings, id, issued.token, trace);
1065
+ return finishPoll(ctx, client, settings, amount, snapshot, polled, trace);
1066
+ });
1067
+ };
1068
+ const query = (ctx, result) => {
1069
+ const amount = result.amount;
1070
+ const snapshot = snapshotOf(result);
1071
+ const trace = createRedebanTrace(now);
1072
+ trace.note(`reference=${ctx.attemptReference}`);
1073
+ return runLogged("redeban.query", ctx, amount, snapshot, trace, "query", () => false, false, async () => {
1074
+ if (!snapshot) {
1075
+ trace.note("missing id");
1076
+ const error = createPaymentError("INVALID_REQUEST", {
1077
+ attempt: ctx.attempt,
1078
+ message: "Missing transaction id"
1079
+ });
1080
+ return withError(ctx, amount, null, error);
1081
+ }
1082
+ const read = readSettings(ctx, trace);
1083
+ if (!read.ok) return withError(ctx, amount, snapshot, read.error);
1084
+ const token = await refresh(read.settings, ctx, trace);
1085
+ const id = snapshot.id;
1086
+ trace.call(`query id=${id}`);
1087
+ const response = await redebanDebugCall("redeban.query", { id }, () => {
1088
+ return client.response({
1089
+ baseUrl: read.settings.baseUrl,
1090
+ codigoUnico: read.settings.codigoUnico,
1091
+ id,
1092
+ token
1093
+ });
1094
+ });
1095
+ trace.result(`respuesta ${response.code}`);
1096
+ const moving = response.code === "00" || response.code === "01" || response.code === "02";
1097
+ const financial = isFinancialMessage(response.message);
1098
+ if (response.code === "00" && financial) return finishFinancial(ctx, amount, snapshot, response.message, trace);
1099
+ if (moving) {
1100
+ trace.note(`pending ${response.code}`);
1101
+ const error = createPaymentError("AMBIGUOUS", { attempt: ctx.attempt });
1102
+ return withError(ctx, amount, snapshot, error, true);
1103
+ }
1104
+ return withError(ctx, amount, snapshot, technicalError(ctx, response));
1105
+ });
1106
+ };
1107
+ const cancel = (ctx, result) => {
1108
+ const amount = result.amount;
1109
+ const snapshot = snapshotOf(result);
1110
+ const wasPaid = result.status === "PAID";
1111
+ const trace = createRedebanTrace(now);
1112
+ trace.note(`reference=${ctx.attemptReference}`);
1113
+ return runLogged("redeban.cancel", ctx, amount, snapshot, trace, "void", () => false, wasPaid, async () => {
1114
+ const read = readSettings(ctx, trace);
1115
+ if (!read.ok || !snapshot) {
1116
+ trace.note("reversal not allowed");
1117
+ const error = createPaymentError("REVERSAL_NOT_ALLOWED", { attempt: ctx.attempt });
1118
+ return withError(ctx, amount, snapshot, error);
1119
+ }
1120
+ const { settings } = read;
1121
+ if (result.status !== "PAID") {
1122
+ if (result.metadata.provider?.last_code === "02") {
1123
+ trace.note("last=02");
1124
+ const error = createPaymentError("AMBIGUOUS", { attempt: ctx.attempt });
1125
+ return withError(ctx, amount, snapshot, error, true);
1126
+ }
1127
+ const token = await refresh(settings, ctx, trace);
1128
+ await erase(client, settings, snapshot.id, token, trace);
1129
+ const error = createPaymentError("CANCELLED_BY_USER", { attempt: ctx.attempt });
1130
+ return {
1131
+ ...withError(ctx, amount, snapshot, error),
1132
+ status: "CANCELLED"
1133
+ };
1134
+ }
1135
+ const id = transactionId(`${snapshot.id}V`);
1136
+ const receipt = snapshot.receipt;
1137
+ const voidAmount = snapshot.amount;
1138
+ const frame = {
1139
+ amount: snapshot.amount,
1140
+ iva: snapshot.iva,
1141
+ invoice: id,
1142
+ cashierCode: settings.cashierCode,
1143
+ terminalId: settings.terminalId,
1144
+ registerNumber: settings.registerNumber,
1145
+ codigoUnico: settings.codigoUnico,
1146
+ receipt: snapshot.receipt,
1147
+ supervisorPassword: settings.supervisorPassword
1148
+ };
1149
+ const token = await refresh(settings, ctx, trace);
1150
+ const voidLine = `id=${id} receipt=${receipt} amount=${voidAmount}`;
1151
+ trace.call(`void ${voidLine}`);
1152
+ const sent = await redebanDebugCall("redeban.send", { id }, () => {
1153
+ return client.send({
1154
+ baseUrl: settings.baseUrl,
1155
+ codigoUnico: settings.codigoUnico,
1156
+ id,
1157
+ data: voidFrame(frame),
1158
+ token
1159
+ });
1160
+ });
1161
+ trace.result(`enviar ${sent.code}`);
1162
+ if (sent.code !== "00") return withError(ctx, amount, snapshot, technicalError(ctx, sent));
1163
+ const polled = await poll(ctx, settings, id, token, trace);
1164
+ const done = await finishPoll(ctx, client, settings, amount, snapshot, polled, trace);
1165
+ if (done.status !== "PAID") return done;
1166
+ return {
1167
+ ...done,
1168
+ status: "CANCELLED"
1169
+ };
1170
+ });
1171
+ };
1172
+ return {
1173
+ provider: "redeban",
1174
+ method: "PIN_PAD",
1175
+ deviceFields: redebanPinPadDeviceFields,
1176
+ errors: redebanPinPadErrors,
1177
+ capabilities: {
1178
+ deferredCapture: false,
1179
+ inPerson: true
1180
+ },
1181
+ policy: { timeout_ms: REDEBAN_TIMEOUT_MS },
1182
+ reversalFor: () => "cancel",
1183
+ start,
1184
+ query,
1185
+ cancel
1186
+ };
1187
+ };
1188
+ //#endregion
1189
+ export { createRedebanClient, redebanPinPadDeviceFields, redebanPinPadErrors, redebanPinPadPayments };
package/package.json CHANGED
@@ -1,6 +1,61 @@
1
1
  {
2
2
  "name": "@fire-spark/payments-redeban-pinpad",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "version": "0.13.0",
4
+ "description": "Fire Spark Redeban pinpad adapter. SIPConnector V1.5 sale, full void, polling and delete.",
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/core": "^0.13.0",
21
+ "@fire-spark/payments": "^0.13.0",
22
+ "@fire-spark/types": "^0.13.0"
23
+ },
24
+ "devDependencies": {
25
+ "tsdown": "^0.22.14",
26
+ "typescript": "^7.0.2",
27
+ "vitest": "^2.1.8"
28
+ },
29
+ "publishConfig": {
30
+ "access": "public"
31
+ },
32
+ "tsdown": {
33
+ "entry": [
34
+ "src/index.ts"
35
+ ],
36
+ "format": [
37
+ "esm"
38
+ ],
39
+ "platform": "neutral",
40
+ "dts": {
41
+ "tsconfig": "tsconfig.build.json"
42
+ },
43
+ "clean": true,
44
+ "deps": {
45
+ "neverBundle": [
46
+ "@fire-spark/core",
47
+ "@fire-spark/payments",
48
+ "@fire-spark/types"
49
+ ]
50
+ }
51
+ },
52
+ "scripts": {
53
+ "build": "tsdown",
54
+ "dev": "tsdown --watch",
55
+ "check-types": "tsc --noEmit",
56
+ "test:utils": "vitest run",
57
+ "lint": "oxlint ./src --max-warnings=0",
58
+ "format": "oxfmt ./src",
59
+ "check-format": "oxfmt --check ./src"
60
+ }
6
61
  }
package/README.md DELETED
@@ -1,3 +0,0 @@
1
- # Temporary Holding Version
2
-
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.