@fire-spark/payments-clisitef-pinpad-rn 0.7.2 → 0.8.1
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 +131 -10
- package/dist/index.js +811 -205
- package/package.json +5 -5
package/dist/index.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { PaymentError, createPaymentError, definePaymentErrors, toPaymentError } from "@fire-spark/payments";
|
|
2
|
-
import { CLISITEF_ABORT_FLAG, CLISITEF_CONFIRM, CLISITEF_MODALITY, CLISITEF_RESULT, CLISITEF_TIMEOUT_CODE, clisitef, clisitefAdditionalParams, clisitefDebug, clisitefDebugCall, clisitefFiscalDateTime, formatClisitefAmount, parseClisitefMenu } from "@fire-spark/payments-clisitef-client";
|
|
2
|
+
import { CLISITEF_ABORT_FLAG, CLISITEF_CONFIRM, CLISITEF_MODALITY, CLISITEF_RESULT, CLISITEF_TIMEOUT_CODE, CLISITEF_VOID_FUNCTION, clisitef, clisitefAdditionalParams, clisitefDebug, clisitefDebugCall, clisitefFiscalDateTime, formatClisitefAmount, parseClisitefMenu } from "@fire-spark/payments-clisitef-client";
|
|
3
3
|
//#region src/constants.ts
|
|
4
4
|
/**
|
|
5
5
|
* `onData` commands handled by the adapter (Fiserv "Tabela de códigos de
|
|
6
6
|
* comando"). Not handled, so they abort the transaction: 29 (silent field),
|
|
7
|
-
* 31 (check), 35 (barcode)
|
|
8
|
-
* `ItemMenuIdentificado`)
|
|
7
|
+
* 31 (check), 35 (barcode) and 42 (identified menu, only with
|
|
8
|
+
* `ItemMenuIdentificado`).
|
|
9
9
|
*/
|
|
10
10
|
const CLISITEF_COMMAND = {
|
|
11
11
|
STORE_FIELD: 0,
|
|
@@ -26,6 +26,9 @@ const CLISITEF_COMMAND = {
|
|
|
26
26
|
READ_FIELD: 30,
|
|
27
27
|
READ_AMOUNT: 34,
|
|
28
28
|
READ_MASKED: 41,
|
|
29
|
+
/** PIX: show the QR (`fieldId` 584 carries the copy-and-paste payload). */
|
|
30
|
+
QR_SHOW: 50,
|
|
31
|
+
QR_HIDE: 51,
|
|
29
32
|
FOOTER: 52
|
|
30
33
|
};
|
|
31
34
|
/** `continueTransaction` answers to `CONFIRM` (20). */
|
|
@@ -43,9 +46,17 @@ const CLISITEF_FIELD = {
|
|
|
43
46
|
AUTHORIZATION_CODE: 135,
|
|
44
47
|
CARD_BIN: 136,
|
|
45
48
|
INSTITUTION_NAME: 156,
|
|
49
|
+
QR_PAYLOAD: 584,
|
|
46
50
|
LAST4: 1190,
|
|
47
51
|
ENTRY_MODE: 2090
|
|
48
52
|
};
|
|
53
|
+
/** Fields a void asks for (`READ_*` commands), answered without the UI. */
|
|
54
|
+
const CLISITEF_VOID_FIELD = {
|
|
55
|
+
AMOUNT: 146,
|
|
56
|
+
SUPERVISOR_PASSWORD: 500,
|
|
57
|
+
ORIGINAL_DATE: 515,
|
|
58
|
+
ORIGINAL_HOST_NSU: 516
|
|
59
|
+
};
|
|
49
60
|
/** Fields that prove the card was read: an error after them is ambiguous. */
|
|
50
61
|
const CLISITEF_CAPTURE_FIELDS = /* @__PURE__ */ new Set([
|
|
51
62
|
CLISITEF_FIELD.CUSTOMER_RECEIPT,
|
|
@@ -80,6 +91,12 @@ const CLISITEF_PAYMENT_TIMEOUT_MS = 18e4;
|
|
|
80
91
|
const CLISITEF_DEFAULT_OPERATOR = "KIOSKO_BR";
|
|
81
92
|
/** Device-wide record of the last transaction, to undo it after a crash. */
|
|
82
93
|
const CLISITEF_STORAGE_KEY = "clisitef:transaction:v1";
|
|
94
|
+
/** Device-wide record of the last refund, apart from the charge one. */
|
|
95
|
+
const CLISITEF_REFUND_STORAGE_KEY = "clisitef:refund:v1";
|
|
96
|
+
/** Device-wide queue of voids that did not complete, retried later. */
|
|
97
|
+
const CLISITEF_REVERSALS_KEY = "clisitef:reversals:v1";
|
|
98
|
+
/** Device-wide record of completed voids, by attempt reference. */
|
|
99
|
+
const CLISITEF_REFUNDS_KEY = "clisitef:refunds:v1";
|
|
83
100
|
//#endregion
|
|
84
101
|
//#region src/errors.ts
|
|
85
102
|
const source = "docs/payments/clisitef/pinpad.md#11-errores";
|
|
@@ -110,14 +127,18 @@ const clisitefPinPadErrors = definePaymentErrors({
|
|
|
110
127
|
"-15": entry("CANCELLED_BY_USER"),
|
|
111
128
|
"-20": invalid("Invalid CliSiTef parameter"),
|
|
112
129
|
"-21": invalid("Prohibited word in pinpad collection"),
|
|
113
|
-
"-24":
|
|
130
|
+
"-24": entry("WALLET_REJECTED", {
|
|
131
|
+
message: "Digital wallet not enabled",
|
|
132
|
+
retryable: false
|
|
133
|
+
}),
|
|
114
134
|
"-30": entry("TERMINAL_UNAVAILABLE", { message: "File access error" }),
|
|
115
135
|
"-40": entry("CARD_DECLINED", { message: "Declined by the SiTef server" }),
|
|
116
136
|
"-41": invalid("Invalid data"),
|
|
117
137
|
"-43": entry("TERMINAL_UNAVAILABLE", { message: "Pinpad routine failed" }),
|
|
118
138
|
"-50": entry("FRAUD_SUSPECTED", { message: "Unsecured transaction" }),
|
|
119
139
|
"-100": entry("UNKNOWN", { message: "CliSiTef internal error" }),
|
|
120
|
-
[CLISITEF_TIMEOUT_CODE]: entry("TIMEOUT")
|
|
140
|
+
[CLISITEF_TIMEOUT_CODE]: entry("TIMEOUT"),
|
|
141
|
+
FINANCING_ONLY: invalid("Only deferred payment offered by SiTef")
|
|
121
142
|
});
|
|
122
143
|
//#endregion
|
|
123
144
|
//#region src/metadata.ts
|
|
@@ -164,27 +185,43 @@ const CARD_TYPES = {
|
|
|
164
185
|
VOUCHER: "PREPAID",
|
|
165
186
|
PIX: null
|
|
166
187
|
};
|
|
167
|
-
|
|
188
|
+
/** A PIX charge has no card: never take brand or last 4 from its receipt. */
|
|
189
|
+
const cardOf = (input, receipt) => {
|
|
168
190
|
const { fields } = input;
|
|
191
|
+
const entry = field(fields, CLISITEF_FIELD.ENTRY_MODE);
|
|
192
|
+
if (input.modality === "PIX") return {
|
|
193
|
+
brand: null,
|
|
194
|
+
last4: null,
|
|
195
|
+
entry_mode: "QR"
|
|
196
|
+
};
|
|
197
|
+
const last4 = field(fields, CLISITEF_FIELD.LAST4)?.slice(-4) ?? null;
|
|
198
|
+
return {
|
|
199
|
+
brand: clisitefBrand(fields),
|
|
200
|
+
last4: last4 ?? clisitefLast4(receipt),
|
|
201
|
+
entry_mode: entry ? ENTRY_MODES[entry] ?? null : null
|
|
202
|
+
};
|
|
203
|
+
};
|
|
204
|
+
const receiptOf = (fields) => {
|
|
169
205
|
const customer = field(fields, CLISITEF_FIELD.CUSTOMER_RECEIPT);
|
|
170
206
|
const merchant = field(fields, CLISITEF_FIELD.MERCHANT_RECEIPT);
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
207
|
+
if (customer === null && merchant === null) return void 0;
|
|
208
|
+
return {
|
|
209
|
+
customer,
|
|
210
|
+
merchant,
|
|
211
|
+
format: "text"
|
|
212
|
+
};
|
|
213
|
+
};
|
|
214
|
+
const clisitefMetadata = (input) => {
|
|
215
|
+
const { fields } = input;
|
|
216
|
+
const receipt = receiptOf(fields);
|
|
174
217
|
const institution = field(fields, CLISITEF_FIELD.INSTITUTION_NAME);
|
|
175
218
|
const p1 = {
|
|
176
219
|
provider_transaction_id: field(fields, CLISITEF_FIELD.SITEF_NSU),
|
|
177
220
|
authorization_code: field(fields, CLISITEF_FIELD.AUTHORIZATION_CODE),
|
|
178
221
|
reference: field(fields, CLISITEF_FIELD.HOST_NSU)
|
|
179
222
|
};
|
|
180
|
-
const p2 = {
|
|
181
|
-
|
|
182
|
-
brand
|
|
183
|
-
};
|
|
184
|
-
const p3 = {
|
|
185
|
-
last4: last4 ?? clisitefLast4(customer ?? merchant),
|
|
186
|
-
entry_mode: entry ? ENTRY_MODES[entry] ?? null : null
|
|
187
|
-
};
|
|
223
|
+
const p2 = { card_type: CARD_TYPES[input.modality] ?? null };
|
|
224
|
+
const p3 = cardOf(input, receipt?.customer ?? receipt?.merchant ?? null);
|
|
188
225
|
const p4 = { acquirer: institution ?? field(fields, CLISITEF_FIELD.INSTITUTION_CODE) };
|
|
189
226
|
const p5 = { terminal_id: input.terminalNumber };
|
|
190
227
|
const p6 = { ticket_number: input.fiscalCoupon };
|
|
@@ -198,17 +235,39 @@ const clisitefMetadata = (input) => {
|
|
|
198
235
|
...p6,
|
|
199
236
|
...p7
|
|
200
237
|
};
|
|
201
|
-
const
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
}
|
|
238
|
+
const fn = CLISITEF_MODALITY[input.modality];
|
|
239
|
+
const provider = {
|
|
240
|
+
modality: input.modality,
|
|
241
|
+
function: fn
|
|
242
|
+
};
|
|
206
243
|
return {
|
|
207
244
|
attempt_reference: input.attemptReference,
|
|
208
245
|
payment,
|
|
209
|
-
receipt
|
|
246
|
+
receipt,
|
|
247
|
+
provider
|
|
248
|
+
};
|
|
249
|
+
};
|
|
250
|
+
/** Void data kept under `provider.void`, next to the charge's metadata. */
|
|
251
|
+
const clisitefVoidMetadata = (input) => {
|
|
252
|
+
const { fields } = input;
|
|
253
|
+
return {
|
|
254
|
+
function: input.function,
|
|
255
|
+
provider_transaction_id: field(fields, CLISITEF_FIELD.SITEF_NSU),
|
|
256
|
+
reference: field(fields, CLISITEF_FIELD.HOST_NSU),
|
|
257
|
+
ticket_number: input.fiscalCoupon,
|
|
258
|
+
transaction_date: input.fiscalDate,
|
|
259
|
+
receipt: receiptOf(fields) ?? null
|
|
210
260
|
};
|
|
211
261
|
};
|
|
262
|
+
/** Spark modality of a charge; older charges only kept `card_type`. */
|
|
263
|
+
const clisitefModalityOf = (metadata) => {
|
|
264
|
+
const stored = metadata.provider?.modality;
|
|
265
|
+
if (typeof stored === "string" && stored in CLISITEF_MODALITY) return stored;
|
|
266
|
+
const cardType = metadata.payment?.card_type;
|
|
267
|
+
if (cardType === "PREPAID") return "VOUCHER";
|
|
268
|
+
if (cardType === "DEBIT" || cardType === "CREDIT") return cardType;
|
|
269
|
+
return null;
|
|
270
|
+
};
|
|
212
271
|
/** PIN_PAD tender details; `null` without brand and last 4 (schema minimum). */
|
|
213
272
|
const clisitefDetails = (metadata) => {
|
|
214
273
|
const payment = metadata.payment;
|
|
@@ -225,138 +284,115 @@ const clisitefDetails = (metadata) => {
|
|
|
225
284
|
};
|
|
226
285
|
};
|
|
227
286
|
//#endregion
|
|
228
|
-
//#region src/
|
|
229
|
-
const
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
* the device values only override them.
|
|
238
|
-
*/
|
|
239
|
-
const clisitefPinPadDeviceFields = [{
|
|
240
|
-
key: "terminal_number",
|
|
241
|
-
label: "SiTef terminal number",
|
|
242
|
-
type: "text",
|
|
243
|
-
required: true
|
|
244
|
-
}, {
|
|
245
|
-
key: "pinpad_type",
|
|
246
|
-
label: "Pinpad connection",
|
|
247
|
-
type: "select",
|
|
248
|
-
options: [...CLISITEF_PINPAD_TYPES]
|
|
249
|
-
}];
|
|
250
|
-
/** The API only sends the entry of the kiosk's store. */
|
|
251
|
-
const storeSettingsOf = (config) => {
|
|
252
|
-
const stores = config.stores;
|
|
253
|
-
if (!stores || typeof stores !== "object") return {};
|
|
254
|
-
const [first] = Object.values(stores);
|
|
255
|
-
return first && typeof first === "object" ? first : {};
|
|
287
|
+
//#region src/reversals.ts
|
|
288
|
+
const readJson = async (storage, key) => {
|
|
289
|
+
const raw = await storage.get(key);
|
|
290
|
+
if (!raw) return null;
|
|
291
|
+
try {
|
|
292
|
+
return JSON.parse(raw);
|
|
293
|
+
} catch {
|
|
294
|
+
return null;
|
|
295
|
+
}
|
|
256
296
|
};
|
|
257
|
-
const
|
|
258
|
-
|
|
297
|
+
const readPendingReversals = async (storage) => {
|
|
298
|
+
const list = await readJson(storage, CLISITEF_REVERSALS_KEY);
|
|
299
|
+
return Array.isArray(list) ? list : [];
|
|
259
300
|
};
|
|
260
|
-
const
|
|
261
|
-
|
|
301
|
+
const writePendingReversals = async (storage, list) => {
|
|
302
|
+
if (!list.length) return storage.remove(CLISITEF_REVERSALS_KEY);
|
|
303
|
+
await storage.set(CLISITEF_REVERSALS_KEY, JSON.stringify(list));
|
|
262
304
|
};
|
|
263
|
-
|
|
264
|
-
|
|
305
|
+
/** Adds the void or bumps its attempts. */
|
|
306
|
+
const queueReversal = async (storage, entry, now) => {
|
|
307
|
+
const list = await readPendingReversals(storage);
|
|
308
|
+
const reference = entry.attemptReference;
|
|
309
|
+
const previous = list.find((item) => item.attemptReference === reference);
|
|
310
|
+
const rest = list.filter((item) => item.attemptReference !== reference);
|
|
311
|
+
const attempts = (previous?.attempts ?? 0) + 1;
|
|
312
|
+
const createdAt = previous?.createdAt ?? now;
|
|
313
|
+
await writePendingReversals(storage, [...rest, {
|
|
314
|
+
...entry,
|
|
315
|
+
attempts,
|
|
316
|
+
createdAt
|
|
317
|
+
}]);
|
|
265
318
|
};
|
|
266
|
-
|
|
267
|
-
const
|
|
268
|
-
const
|
|
269
|
-
|
|
319
|
+
const dropReversal = async (storage, attemptReference) => {
|
|
320
|
+
const list = await readPendingReversals(storage);
|
|
321
|
+
const rest = list.filter((item) => item.attemptReference !== attemptReference);
|
|
322
|
+
if (rest.length !== list.length) await writePendingReversals(storage, rest);
|
|
270
323
|
};
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
const raw = config.restrictions_by_modality;
|
|
274
|
-
const byModality = raw && typeof raw === "object" ? raw : {};
|
|
275
|
-
const legacy = text(config.restrictions);
|
|
276
|
-
const entries = modalities.map((modality) => {
|
|
277
|
-
return [modality, text(byModality[modality]) || legacy];
|
|
278
|
-
});
|
|
279
|
-
return Object.fromEntries(entries.filter(([, value]) => value));
|
|
324
|
+
const readRefund = async (storage, attemptReference) => {
|
|
325
|
+
return (await readJson(storage, "clisitef:refunds:v1"))?.[attemptReference] ?? null;
|
|
280
326
|
};
|
|
281
|
-
|
|
282
|
-
const
|
|
283
|
-
const
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
const
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
327
|
+
const writeRefund = async (storage, attemptReference, result) => {
|
|
328
|
+
const refunds = await readJson(storage, "clisitef:refunds:v1") ?? {};
|
|
329
|
+
const kept = Object.entries(refunds).filter(([key]) => {
|
|
330
|
+
return key !== attemptReference;
|
|
331
|
+
}).slice(-49);
|
|
332
|
+
const next = Object.fromEntries([...kept, [attemptReference, result]]);
|
|
333
|
+
await storage.set(CLISITEF_REFUNDS_KEY, JSON.stringify(next));
|
|
334
|
+
};
|
|
335
|
+
//#endregion
|
|
336
|
+
//#region src/menu.ts
|
|
337
|
+
const normalize = (label) => {
|
|
338
|
+
return label.normalize("NFD").replace(/[\u0300-\u036f]/g, "").toUpperCase();
|
|
339
|
+
};
|
|
340
|
+
/** No voucher matcher: its brand menu is always shown. */
|
|
341
|
+
const MATCHERS = {
|
|
342
|
+
DEBIT: /DEBITO/,
|
|
343
|
+
CREDIT: /CREDITO/,
|
|
344
|
+
PIX: /PIX|QR|DIGITAL/
|
|
345
|
+
};
|
|
346
|
+
/** Debit and credit are already picked in the kiosk UI before the charge. */
|
|
347
|
+
const PRESELECTED = /* @__PURE__ */ new Set(["DEBIT", "CREDIT"]);
|
|
348
|
+
const SPOT = /A VISTA/;
|
|
349
|
+
const FINANCING = /PARCEL|PRE[\s-]?DATAD|CREDIARIO|JUROS|DIFERID/;
|
|
350
|
+
const matching = (options, pattern) => {
|
|
351
|
+
return options.filter((option) => pattern.test(normalize(option.label)));
|
|
352
|
+
};
|
|
353
|
+
/** The kiosk has no deferred payment: spot over financing, never financing. */
|
|
354
|
+
const chargeMenu = (options) => {
|
|
355
|
+
const financing = matching(options, FINANCING);
|
|
356
|
+
if (!financing.length) return {
|
|
357
|
+
kind: "ask",
|
|
358
|
+
options
|
|
306
359
|
};
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
clisitefDebug("pinpad.settings", "read", {
|
|
310
|
-
config,
|
|
311
|
-
device,
|
|
312
|
-
missing
|
|
360
|
+
const [spot] = matching(options, SPOT).filter((option) => {
|
|
361
|
+
return !financing.includes(option);
|
|
313
362
|
});
|
|
314
|
-
if (
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
cnpjEnterprise,
|
|
323
|
-
pinpadType
|
|
324
|
-
})
|
|
363
|
+
if (spot) return {
|
|
364
|
+
kind: "pick",
|
|
365
|
+
value: spot.value
|
|
366
|
+
};
|
|
367
|
+
const rest = options.filter((option) => !financing.includes(option));
|
|
368
|
+
if (!rest.length) return {
|
|
369
|
+
kind: "abort",
|
|
370
|
+
message: "Only deferred payment offered"
|
|
325
371
|
};
|
|
326
|
-
const operator = text(config.operator) || "KIOSKO_BR";
|
|
327
|
-
const unique = [...new Set(listed)];
|
|
328
372
|
return {
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
operator,
|
|
332
|
-
restrictions: readRestrictions(config, unique),
|
|
333
|
-
terminalNumber
|
|
373
|
+
kind: "ask",
|
|
374
|
+
options: rest
|
|
334
375
|
};
|
|
335
376
|
};
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
const
|
|
344
|
-
const
|
|
345
|
-
|
|
346
|
-
if (
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
};
|
|
354
|
-
|
|
355
|
-
clisitefDebug("pinpad.store", `write ${record.state}`, record);
|
|
356
|
-
await storage.set(CLISITEF_STORAGE_KEY, JSON.stringify(record));
|
|
357
|
-
};
|
|
358
|
-
const isOpen = (record) => {
|
|
359
|
-
return record.state === "started" || record.state === "approved";
|
|
377
|
+
/**
|
|
378
|
+
* Command 21. Labels compare without accents and in upper case. A void
|
|
379
|
+
* picks the original modality when one option matches it; a charge does it
|
|
380
|
+
* only for debit and credit, then drops the financing options.
|
|
381
|
+
*/
|
|
382
|
+
const resolveClisitefMenu = (options, modality, purpose) => {
|
|
383
|
+
const matcher = MATCHERS[modality];
|
|
384
|
+
const preselected = purpose === "void" || PRESELECTED.has(modality);
|
|
385
|
+
const matched = matcher && preselected ? matching(options, matcher) : [];
|
|
386
|
+
const [only] = matched;
|
|
387
|
+
if (matched.length === 1 && only) return {
|
|
388
|
+
kind: "pick",
|
|
389
|
+
value: only.value
|
|
390
|
+
};
|
|
391
|
+
if (purpose === "void") return {
|
|
392
|
+
kind: "ask",
|
|
393
|
+
options
|
|
394
|
+
};
|
|
395
|
+
return chargeMenu(matched.length ? matched : options);
|
|
360
396
|
};
|
|
361
397
|
//#endregion
|
|
362
398
|
//#region src/transaction.ts
|
|
@@ -373,6 +409,11 @@ const inputType = (command) => {
|
|
|
373
409
|
if (command === CLISITEF_COMMAND.READ_AMOUNT) return "number";
|
|
374
410
|
return "text";
|
|
375
411
|
};
|
|
412
|
+
/** `"AGUARDE, EM PROCESSAMENTO (3)"` → `"AGUARDE, EM PROCESSAMENTO"`. */
|
|
413
|
+
const COUNTER = /\s*\(\s*\d+\s*\)\s*$/;
|
|
414
|
+
const collapseCounter = (text) => {
|
|
415
|
+
return text.replace(COUNTER, "").trim() || text;
|
|
416
|
+
};
|
|
376
417
|
const MESSAGES = /* @__PURE__ */ new Set([
|
|
377
418
|
CLISITEF_COMMAND.MESSAGE_OPERATOR,
|
|
378
419
|
CLISITEF_COMMAND.MESSAGE_CUSTOMER,
|
|
@@ -393,6 +434,16 @@ const READS = /* @__PURE__ */ new Set([
|
|
|
393
434
|
CLISITEF_COMMAND.READ_AMOUNT,
|
|
394
435
|
CLISITEF_COMMAND.READ_MASKED
|
|
395
436
|
]);
|
|
437
|
+
/** The PIX payload is the charge code and the supervisor password a secret. */
|
|
438
|
+
const redact = (event) => {
|
|
439
|
+
const qr = event.command === CLISITEF_COMMAND.QR_SHOW;
|
|
440
|
+
const masked = event.command === CLISITEF_COMMAND.READ_MASKED;
|
|
441
|
+
if (!qr && !masked && event.fieldId !== CLISITEF_FIELD.QR_PAYLOAD) return event;
|
|
442
|
+
return {
|
|
443
|
+
...event,
|
|
444
|
+
input: `<${event.input.length} chars>`
|
|
445
|
+
};
|
|
446
|
+
};
|
|
396
447
|
/**
|
|
397
448
|
* One CliSiTef transaction: `startTransaction`, the `onData` loop answered
|
|
398
449
|
* through `ui`, `finishTransaction(1)` on approval (`stage 1` = 0) and
|
|
@@ -403,6 +454,7 @@ const runClisitefTransaction = (input) => {
|
|
|
403
454
|
const fields = /* @__PURE__ */ new Map();
|
|
404
455
|
const subscriptions = [];
|
|
405
456
|
let title = "";
|
|
457
|
+
let lastMessage = "";
|
|
406
458
|
let captured = false;
|
|
407
459
|
let approved = false;
|
|
408
460
|
let closing = false;
|
|
@@ -426,7 +478,7 @@ const runClisitefTransaction = (input) => {
|
|
|
426
478
|
const settle = (status, reason) => {
|
|
427
479
|
log(`settle ${status}`, {
|
|
428
480
|
...reason,
|
|
429
|
-
|
|
481
|
+
fieldIds: [...fields.keys()]
|
|
430
482
|
});
|
|
431
483
|
clearTimeout(watchdog);
|
|
432
484
|
for (const subscription of subscriptions) subscription.remove();
|
|
@@ -435,7 +487,8 @@ const runClisitefTransaction = (input) => {
|
|
|
435
487
|
status,
|
|
436
488
|
...reason,
|
|
437
489
|
fields,
|
|
438
|
-
aborted
|
|
490
|
+
aborted,
|
|
491
|
+
evidence: captured
|
|
439
492
|
});
|
|
440
493
|
};
|
|
441
494
|
const undo = async (reason) => {
|
|
@@ -470,17 +523,73 @@ const runClisitefTransaction = (input) => {
|
|
|
470
523
|
message: "Aborted by the payment deadline"
|
|
471
524
|
});
|
|
472
525
|
}
|
|
473
|
-
const reply = async (data) => {
|
|
526
|
+
const reply = async (data, hidden = false) => {
|
|
474
527
|
log("reply", {
|
|
475
|
-
data,
|
|
528
|
+
data: hidden ? "********" : data,
|
|
529
|
+
length: data.length,
|
|
476
530
|
skipped: closing
|
|
477
531
|
});
|
|
478
532
|
if (closing) return;
|
|
479
533
|
await client.continueTransaction(data);
|
|
480
534
|
};
|
|
535
|
+
const showMessage = (text) => {
|
|
536
|
+
const collapsed = collapseCounter(text.trim());
|
|
537
|
+
if (!collapsed || collapsed === lastMessage) return;
|
|
538
|
+
lastMessage = collapsed;
|
|
539
|
+
ui.message({
|
|
540
|
+
text: collapsed,
|
|
541
|
+
kind: "waiting"
|
|
542
|
+
});
|
|
543
|
+
};
|
|
544
|
+
const onMenu = async (text) => {
|
|
545
|
+
const options = parseClisitefMenu(text);
|
|
546
|
+
const label = title || "Selecione uma opção";
|
|
547
|
+
const decision = resolveClisitefMenu(...[
|
|
548
|
+
options,
|
|
549
|
+
input.modality,
|
|
550
|
+
input.purpose
|
|
551
|
+
]);
|
|
552
|
+
log("menu", {
|
|
553
|
+
title: label,
|
|
554
|
+
options,
|
|
555
|
+
decision
|
|
556
|
+
});
|
|
557
|
+
if (decision.kind === "abort") {
|
|
558
|
+
const reason = {
|
|
559
|
+
code: "FINANCING_ONLY",
|
|
560
|
+
message: decision.message
|
|
561
|
+
};
|
|
562
|
+
return close(reason);
|
|
563
|
+
}
|
|
564
|
+
if (decision.kind === "pick") return reply(decision.value);
|
|
565
|
+
const p2 = {
|
|
566
|
+
title: label,
|
|
567
|
+
options: decision.options
|
|
568
|
+
};
|
|
569
|
+
const picked = await ui.select(p2);
|
|
570
|
+
log("ui.select ← out", { picked });
|
|
571
|
+
return reply(picked);
|
|
572
|
+
};
|
|
573
|
+
const onQr = (event) => {
|
|
574
|
+
const payload = event.input.trim();
|
|
575
|
+
const isPayload = event.fieldId === CLISITEF_FIELD.QR_PAYLOAD;
|
|
576
|
+
log("qr shown", {
|
|
577
|
+
fieldId: event.fieldId,
|
|
578
|
+
length: payload.length
|
|
579
|
+
});
|
|
580
|
+
if (!isPayload || !payload) return reply("");
|
|
581
|
+
captured = true;
|
|
582
|
+
ui.action({
|
|
583
|
+
type: "qr",
|
|
584
|
+
payload,
|
|
585
|
+
format: "text",
|
|
586
|
+
expiresAt: null
|
|
587
|
+
});
|
|
588
|
+
return reply("");
|
|
589
|
+
};
|
|
481
590
|
const onData = async (event) => {
|
|
482
591
|
log("onData", {
|
|
483
|
-
...event,
|
|
592
|
+
...redact(event),
|
|
484
593
|
title
|
|
485
594
|
});
|
|
486
595
|
const { command, input: text } = event;
|
|
@@ -489,13 +598,20 @@ const runClisitefTransaction = (input) => {
|
|
|
489
598
|
if (CLISITEF_CAPTURE_FIELDS.has(event.fieldId)) captured = true;
|
|
490
599
|
return reply("");
|
|
491
600
|
}
|
|
492
|
-
if (
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
601
|
+
if (command === CLISITEF_COMMAND.QR_SHOW) return onQr(event);
|
|
602
|
+
if (command === CLISITEF_COMMAND.QR_HIDE) {
|
|
603
|
+
ui.action({
|
|
604
|
+
type: "qr",
|
|
605
|
+
payload: "",
|
|
606
|
+
format: "text",
|
|
607
|
+
expiresAt: null
|
|
496
608
|
});
|
|
497
609
|
return reply("");
|
|
498
610
|
}
|
|
611
|
+
if (MESSAGES.has(command)) {
|
|
612
|
+
showMessage(text);
|
|
613
|
+
return reply("");
|
|
614
|
+
}
|
|
499
615
|
if (command === CLISITEF_COMMAND.MENU_TITLE) {
|
|
500
616
|
title = text.trim();
|
|
501
617
|
return reply("");
|
|
@@ -511,21 +627,10 @@ const runClisitefTransaction = (input) => {
|
|
|
511
627
|
log("ui.confirm ← out", { yes });
|
|
512
628
|
return reply(yes ? "0" : "1");
|
|
513
629
|
}
|
|
514
|
-
if (command === CLISITEF_COMMAND.MENU)
|
|
515
|
-
const options = parseClisitefMenu(text);
|
|
516
|
-
const label = title || "Selecione uma opção";
|
|
517
|
-
log("ui.select → in", {
|
|
518
|
-
title: label,
|
|
519
|
-
options
|
|
520
|
-
});
|
|
521
|
-
const picked = await ui.select({
|
|
522
|
-
title: label,
|
|
523
|
-
options
|
|
524
|
-
});
|
|
525
|
-
log("ui.select ← out", { picked });
|
|
526
|
-
return reply(picked);
|
|
527
|
-
}
|
|
630
|
+
if (command === CLISITEF_COMMAND.MENU) return onMenu(text);
|
|
528
631
|
if (READS.has(command)) {
|
|
632
|
+
const known = input.answerField?.(event.fieldId) ?? null;
|
|
633
|
+
if (known !== null) return reply(known, true);
|
|
529
634
|
const label = text.trim() || title || "Informe o dado";
|
|
530
635
|
const max = event.maxLength > 0 ? event.maxLength : void 0;
|
|
531
636
|
const p1 = {
|
|
@@ -535,7 +640,6 @@ const runClisitefTransaction = (input) => {
|
|
|
535
640
|
};
|
|
536
641
|
log("ui.input → in", p1);
|
|
537
642
|
const value = await ui.input(p1);
|
|
538
|
-
log("ui.input ← out", { value });
|
|
539
643
|
return reply(value);
|
|
540
644
|
}
|
|
541
645
|
const message = `Unsupported CliSiTef command ${command}`;
|
|
@@ -672,6 +776,393 @@ const runClisitefTransaction = (input) => {
|
|
|
672
776
|
return done;
|
|
673
777
|
};
|
|
674
778
|
//#endregion
|
|
779
|
+
//#region src/refund.ts
|
|
780
|
+
/**
|
|
781
|
+
* The acquirer or SiTef answered: the transaction is not there, already
|
|
782
|
+
* voided or the void is refused. Retrying does not help.
|
|
783
|
+
*/
|
|
784
|
+
const DEFINITIVE = /* @__PURE__ */ new Set([
|
|
785
|
+
-3,
|
|
786
|
+
-10,
|
|
787
|
+
-13,
|
|
788
|
+
-20,
|
|
789
|
+
-40,
|
|
790
|
+
-41
|
|
791
|
+
]);
|
|
792
|
+
const isDefinitive = (outcome) => {
|
|
793
|
+
if (outcome.aborted || outcome.error) return false;
|
|
794
|
+
if (outcome.status === "unresolved") return false;
|
|
795
|
+
const { code } = outcome;
|
|
796
|
+
if (typeof code !== "number") return false;
|
|
797
|
+
return code > 0 || DEFINITIVE.has(code);
|
|
798
|
+
};
|
|
799
|
+
/** `YYYYMMDD` (fiscal date of the charge) → `DDMMYYYY` (field 515). */
|
|
800
|
+
const toDdMmYyyy = (date) => {
|
|
801
|
+
return `${date.slice(6, 8)}${date.slice(4, 6)}${date.slice(0, 4)}`;
|
|
802
|
+
};
|
|
803
|
+
const targetOf = (result) => {
|
|
804
|
+
const payment = result.metadata.payment;
|
|
805
|
+
const modality = clisitefModalityOf(result.metadata);
|
|
806
|
+
const hostNsu = payment?.reference?.trim();
|
|
807
|
+
const transactionDate = payment?.transaction_date?.trim();
|
|
808
|
+
if (!modality || !hostNsu || !transactionDate) return null;
|
|
809
|
+
if (!/^\d{8}$/.test(transactionDate)) return null;
|
|
810
|
+
return {
|
|
811
|
+
reference: result.metadata.attempt_reference,
|
|
812
|
+
modality,
|
|
813
|
+
hostNsu,
|
|
814
|
+
transactionDate
|
|
815
|
+
};
|
|
816
|
+
};
|
|
817
|
+
/** The charge stays in force: `PAID`, flagged for review. */
|
|
818
|
+
const stillPaid = (result, error) => {
|
|
819
|
+
const metadata = {
|
|
820
|
+
...result.metadata,
|
|
821
|
+
error,
|
|
822
|
+
needs_review: true
|
|
823
|
+
};
|
|
824
|
+
return {
|
|
825
|
+
...result,
|
|
826
|
+
status: "PAID",
|
|
827
|
+
metadata,
|
|
828
|
+
error
|
|
829
|
+
};
|
|
830
|
+
};
|
|
831
|
+
const voidError = (outcome, attempt) => {
|
|
832
|
+
const p1 = {
|
|
833
|
+
provider_message: outcome.message,
|
|
834
|
+
attempt
|
|
835
|
+
};
|
|
836
|
+
const code = outcome.code == null ? null : String(outcome.code);
|
|
837
|
+
const extra = {
|
|
838
|
+
...p1,
|
|
839
|
+
provider_code: code
|
|
840
|
+
};
|
|
841
|
+
if (isDefinitive(outcome)) {
|
|
842
|
+
const message = "CliSiTef refused the void";
|
|
843
|
+
return createPaymentError("REVERSAL_NOT_ALLOWED", {
|
|
844
|
+
...extra,
|
|
845
|
+
message,
|
|
846
|
+
retryable: false
|
|
847
|
+
});
|
|
848
|
+
}
|
|
849
|
+
if (outcome.aborted || code === null) return createPaymentError("PROVIDER_UNAVAILABLE", {
|
|
850
|
+
...extra,
|
|
851
|
+
retryable: true
|
|
852
|
+
});
|
|
853
|
+
return {
|
|
854
|
+
...toPaymentError(clisitefPinPadErrors, {
|
|
855
|
+
code: outcome.code,
|
|
856
|
+
message: outcome.message,
|
|
857
|
+
attempt,
|
|
858
|
+
fallback: "PROVIDER_UNAVAILABLE"
|
|
859
|
+
}),
|
|
860
|
+
retryable: true
|
|
861
|
+
};
|
|
862
|
+
};
|
|
863
|
+
/**
|
|
864
|
+
* Voids a confirmed charge with its cancellation function (credit 210,
|
|
865
|
+
* debit and voucher 211, PIX 123). `REFUNDED` only after `stage 2` = 0;
|
|
866
|
+
* any other end keeps the charge `PAID` with `needs_review`, and a
|
|
867
|
+
* retryable one goes to the pending queue.
|
|
868
|
+
*/
|
|
869
|
+
const runClisitefVoid = async (deps, ctx, result) => {
|
|
870
|
+
if (result.status === "REFUNDED") return result;
|
|
871
|
+
const reference = result.metadata.attempt_reference;
|
|
872
|
+
const done = await readRefund(ctx.storage, reference);
|
|
873
|
+
if (done) return done;
|
|
874
|
+
const target = targetOf(result);
|
|
875
|
+
if (!target) {
|
|
876
|
+
const error = createPaymentError("MANUAL_REVERSAL_REQUIRED", {
|
|
877
|
+
message: "Missing CliSiTef void context (host NSU, date, modality)",
|
|
878
|
+
retryable: false,
|
|
879
|
+
attempt: ctx.attempt
|
|
880
|
+
});
|
|
881
|
+
return stillPaid(result, error);
|
|
882
|
+
}
|
|
883
|
+
const { settings } = ctx;
|
|
884
|
+
const date = deps.now();
|
|
885
|
+
const queue = async (error, paid) => {
|
|
886
|
+
const r1 = {
|
|
887
|
+
attemptReference: reference,
|
|
888
|
+
amount: result.amount
|
|
889
|
+
};
|
|
890
|
+
const r2 = { transactionDate: target.transactionDate };
|
|
891
|
+
const r3 = {
|
|
892
|
+
hostNsu: target.hostNsu,
|
|
893
|
+
modality: target.modality
|
|
894
|
+
};
|
|
895
|
+
const entry = {
|
|
896
|
+
...r1,
|
|
897
|
+
...r2,
|
|
898
|
+
...r3,
|
|
899
|
+
reason: error.message,
|
|
900
|
+
result
|
|
901
|
+
};
|
|
902
|
+
await queueReversal(ctx.storage, entry, date.getTime());
|
|
903
|
+
return paid;
|
|
904
|
+
};
|
|
905
|
+
if (ctx.unavailable) {
|
|
906
|
+
const error = {
|
|
907
|
+
...ctx.unavailable,
|
|
908
|
+
retryable: true
|
|
909
|
+
};
|
|
910
|
+
return queue(error, stillPaid(result, error));
|
|
911
|
+
}
|
|
912
|
+
const fiscal = clisitefFiscalDateTime(date, deps.timeZone);
|
|
913
|
+
const fiscalCoupon = String(date.getTime());
|
|
914
|
+
const finish = {
|
|
915
|
+
fiscalCoupon,
|
|
916
|
+
...fiscal
|
|
917
|
+
};
|
|
918
|
+
const fn = CLISITEF_VOID_FUNCTION[target.modality];
|
|
919
|
+
const value = formatClisitefAmount(result.amount);
|
|
920
|
+
const s1 = {
|
|
921
|
+
modality: fn,
|
|
922
|
+
value,
|
|
923
|
+
...finish,
|
|
924
|
+
operator: settings.operator
|
|
925
|
+
};
|
|
926
|
+
const answers = {
|
|
927
|
+
[CLISITEF_VOID_FIELD.AMOUNT]: value,
|
|
928
|
+
[CLISITEF_VOID_FIELD.ORIGINAL_DATE]: toDdMmYyyy(target.transactionDate),
|
|
929
|
+
[CLISITEF_VOID_FIELD.ORIGINAL_HOST_NSU]: target.hostNsu
|
|
930
|
+
};
|
|
931
|
+
if (settings.supervisorPassword) answers[CLISITEF_VOID_FIELD.SUPERVISOR_PASSWORD] = settings.supervisorPassword;
|
|
932
|
+
const outcome = await runClisitefTransaction({
|
|
933
|
+
client: deps.client,
|
|
934
|
+
ui: ctx.ui,
|
|
935
|
+
signal: ctx.signal,
|
|
936
|
+
start: {
|
|
937
|
+
...s1,
|
|
938
|
+
restrictions: ""
|
|
939
|
+
},
|
|
940
|
+
finish,
|
|
941
|
+
confirmationTimeoutMs: deps.confirmationTimeoutMs,
|
|
942
|
+
modality: target.modality,
|
|
943
|
+
purpose: "void",
|
|
944
|
+
onApproved: async () => void 0,
|
|
945
|
+
answerField: (fieldId) => answers[fieldId] ?? null
|
|
946
|
+
});
|
|
947
|
+
if (outcome.code === -1) deps.client.invalidate();
|
|
948
|
+
const voided = clisitefVoidMetadata({
|
|
949
|
+
fields: outcome.fields,
|
|
950
|
+
function: fn,
|
|
951
|
+
fiscalCoupon,
|
|
952
|
+
fiscalDate: fiscal.fiscalDate
|
|
953
|
+
});
|
|
954
|
+
const provider = {
|
|
955
|
+
...result.metadata.provider,
|
|
956
|
+
void: voided
|
|
957
|
+
};
|
|
958
|
+
if (outcome.status === "confirmed") {
|
|
959
|
+
const metadata = {
|
|
960
|
+
...result.metadata,
|
|
961
|
+
provider
|
|
962
|
+
};
|
|
963
|
+
delete metadata.error;
|
|
964
|
+
delete metadata.needs_review;
|
|
965
|
+
const refunded = {
|
|
966
|
+
...result,
|
|
967
|
+
status: "REFUNDED",
|
|
968
|
+
metadata,
|
|
969
|
+
error: void 0
|
|
970
|
+
};
|
|
971
|
+
await writeRefund(ctx.storage, reference, refunded);
|
|
972
|
+
await dropReversal(ctx.storage, reference);
|
|
973
|
+
return refunded;
|
|
974
|
+
}
|
|
975
|
+
const error = voidError(outcome, ctx.attempt);
|
|
976
|
+
const withVoid = {
|
|
977
|
+
...result,
|
|
978
|
+
metadata: {
|
|
979
|
+
...result.metadata,
|
|
980
|
+
provider
|
|
981
|
+
}
|
|
982
|
+
};
|
|
983
|
+
const paid = stillPaid(withVoid, error);
|
|
984
|
+
if (!error.retryable) {
|
|
985
|
+
await dropReversal(ctx.storage, reference);
|
|
986
|
+
return paid;
|
|
987
|
+
}
|
|
988
|
+
return queue(error, paid);
|
|
989
|
+
};
|
|
990
|
+
//#endregion
|
|
991
|
+
//#region src/settings.ts
|
|
992
|
+
const CLISITEF_PINPAD_TYPES = [
|
|
993
|
+
"ANDROID_USB",
|
|
994
|
+
"ANDROID_BT",
|
|
995
|
+
"ANDROID_AUTO"
|
|
996
|
+
];
|
|
997
|
+
/**
|
|
998
|
+
* Ficha §6: filled per kiosk in the device settings. SiTef IP, store code
|
|
999
|
+
* and store CNPJ come from the payment's store settings (`config.stores`);
|
|
1000
|
+
* the device values only override them.
|
|
1001
|
+
*/
|
|
1002
|
+
const clisitefPinPadDeviceFields = [
|
|
1003
|
+
{
|
|
1004
|
+
key: "terminal_number",
|
|
1005
|
+
label: "SiTef terminal number",
|
|
1006
|
+
type: "text",
|
|
1007
|
+
required: true
|
|
1008
|
+
},
|
|
1009
|
+
{
|
|
1010
|
+
key: "pinpad_type",
|
|
1011
|
+
label: "Pinpad connection",
|
|
1012
|
+
type: "select",
|
|
1013
|
+
options: [...CLISITEF_PINPAD_TYPES]
|
|
1014
|
+
},
|
|
1015
|
+
{
|
|
1016
|
+
key: "secret_supervisor_password",
|
|
1017
|
+
label: "SiTef supervisor password (voids)",
|
|
1018
|
+
type: "password",
|
|
1019
|
+
secret: true
|
|
1020
|
+
}
|
|
1021
|
+
];
|
|
1022
|
+
const redactSecrets = (values = {}) => {
|
|
1023
|
+
const entries = Object.entries(values).map(([key, value]) => {
|
|
1024
|
+
return [key, (key.startsWith("secret_") || key === "supervisorPassword") && value ? "********" : value];
|
|
1025
|
+
});
|
|
1026
|
+
return Object.fromEntries(entries);
|
|
1027
|
+
};
|
|
1028
|
+
const redactStores = (stores) => {
|
|
1029
|
+
if (!stores || typeof stores !== "object") return stores;
|
|
1030
|
+
const redacted = Object.entries(stores).map(([id, store]) => {
|
|
1031
|
+
return [id, redactSecrets(store && typeof store === "object" ? store : {})];
|
|
1032
|
+
});
|
|
1033
|
+
return Object.fromEntries(redacted);
|
|
1034
|
+
};
|
|
1035
|
+
/** The API only sends the entry of the kiosk's store. */
|
|
1036
|
+
const storeSettingsOf = (config) => {
|
|
1037
|
+
const stores = config.stores;
|
|
1038
|
+
if (!stores || typeof stores !== "object") return {};
|
|
1039
|
+
const [first] = Object.values(stores);
|
|
1040
|
+
return first && typeof first === "object" ? first : {};
|
|
1041
|
+
};
|
|
1042
|
+
const CLISITEF_MODALITIES = [
|
|
1043
|
+
"DEBIT",
|
|
1044
|
+
"CREDIT",
|
|
1045
|
+
"VOUCHER",
|
|
1046
|
+
"PIX"
|
|
1047
|
+
];
|
|
1048
|
+
const text = (value) => {
|
|
1049
|
+
return typeof value === "string" ? value.trim() : "";
|
|
1050
|
+
};
|
|
1051
|
+
const notConfigured = (missing) => {
|
|
1052
|
+
return new PaymentError(createPaymentError("TERMINAL_NOT_CONFIGURED", { message: `CliSiTef is missing ${missing.join(", ")}` }));
|
|
1053
|
+
};
|
|
1054
|
+
const isClisitefModality = (value) => {
|
|
1055
|
+
return CLISITEF_MODALITIES.includes(value);
|
|
1056
|
+
};
|
|
1057
|
+
/** `modalities`, or the legacy single `modality` of older configs. */
|
|
1058
|
+
const listModalities = (config) => {
|
|
1059
|
+
const listed = Array.isArray(config.modalities) ? config.modalities : [];
|
|
1060
|
+
return (listed.length ? listed : [config.modality]).map(text).filter(Boolean);
|
|
1061
|
+
};
|
|
1062
|
+
/**
|
|
1063
|
+
* Per modality. The legacy single `restrictions` only applies to configs
|
|
1064
|
+
* without `restrictions_by_modality`: one modality never borrows another's
|
|
1065
|
+
* list (the credit one makes SiTef reject vouchers with `-3`).
|
|
1066
|
+
*/
|
|
1067
|
+
const readRestrictions = (config, modalities) => {
|
|
1068
|
+
const raw = config.restrictions_by_modality;
|
|
1069
|
+
const hasByModality = !!raw && typeof raw === "object";
|
|
1070
|
+
const byModality = hasByModality ? raw : {};
|
|
1071
|
+
const legacy = hasByModality ? "" : text(config.restrictions);
|
|
1072
|
+
const entries = modalities.map((modality) => {
|
|
1073
|
+
return [modality, text(byModality[modality]) || legacy];
|
|
1074
|
+
});
|
|
1075
|
+
return Object.fromEntries(entries.filter(([, value]) => value));
|
|
1076
|
+
};
|
|
1077
|
+
/** `payments.config` (merchant) + device settings; throws when incomplete. */
|
|
1078
|
+
const readClisitefPinPadSettings = (config, device) => {
|
|
1079
|
+
const listed = listModalities(config);
|
|
1080
|
+
const cnpjEnterprise = text(config.cnpj_enterprise);
|
|
1081
|
+
const store = storeSettingsOf(config);
|
|
1082
|
+
const serverIp = text(device.sitef_ip) || text(store.sitef_ip);
|
|
1083
|
+
const storeCode = text(device.store_code) || text(store.store_code);
|
|
1084
|
+
const terminalNumber = text(device.terminal_number);
|
|
1085
|
+
const cnpjStore = (text(device.cnpj_store) || text(store.cnpj_store)).replace(/\D/g, "");
|
|
1086
|
+
const pinpadType = text(device.pinpad_type) || void 0;
|
|
1087
|
+
const required = {
|
|
1088
|
+
modalities: listed.join(","),
|
|
1089
|
+
cnpj_enterprise: cnpjEnterprise
|
|
1090
|
+
};
|
|
1091
|
+
const fromDevice = {
|
|
1092
|
+
sitef_ip: serverIp,
|
|
1093
|
+
store_code: storeCode
|
|
1094
|
+
};
|
|
1095
|
+
const more = {
|
|
1096
|
+
terminal_number: terminalNumber,
|
|
1097
|
+
cnpj_store: cnpjStore
|
|
1098
|
+
};
|
|
1099
|
+
const values = {
|
|
1100
|
+
...required,
|
|
1101
|
+
...fromDevice
|
|
1102
|
+
};
|
|
1103
|
+
Object.assign(values, more);
|
|
1104
|
+
const missing = Object.keys(values).filter((key) => !values[key]);
|
|
1105
|
+
const shown = redactSecrets(device);
|
|
1106
|
+
clisitefDebug("pinpad.settings", "read", {
|
|
1107
|
+
config: {
|
|
1108
|
+
...config,
|
|
1109
|
+
stores: redactStores(config.stores)
|
|
1110
|
+
},
|
|
1111
|
+
device: shown,
|
|
1112
|
+
missing
|
|
1113
|
+
});
|
|
1114
|
+
if (missing.length) throw notConfigured(missing);
|
|
1115
|
+
if (!listed.every(isClisitefModality)) throw notConfigured(["a valid modality"]);
|
|
1116
|
+
const configure = {
|
|
1117
|
+
serverIp,
|
|
1118
|
+
storeCode,
|
|
1119
|
+
terminalNumber,
|
|
1120
|
+
additionalParams: clisitefAdditionalParams({
|
|
1121
|
+
cnpjStore,
|
|
1122
|
+
cnpjEnterprise,
|
|
1123
|
+
pinpadType
|
|
1124
|
+
})
|
|
1125
|
+
};
|
|
1126
|
+
const operator = text(config.operator) || "KIOSKO_BR";
|
|
1127
|
+
const unique = [...new Set(listed)];
|
|
1128
|
+
const restrictions = readRestrictions(config, unique);
|
|
1129
|
+
const devicePassword = text(device.secret_supervisor_password);
|
|
1130
|
+
const storePassword = text(store.secret_supervisor_password);
|
|
1131
|
+
return {
|
|
1132
|
+
configure,
|
|
1133
|
+
modalities: unique,
|
|
1134
|
+
operator,
|
|
1135
|
+
restrictions,
|
|
1136
|
+
terminalNumber,
|
|
1137
|
+
supervisorPassword: devicePassword || storePassword || ""
|
|
1138
|
+
};
|
|
1139
|
+
};
|
|
1140
|
+
//#endregion
|
|
1141
|
+
//#region src/store.ts
|
|
1142
|
+
const isRecord = (value) => {
|
|
1143
|
+
if (!value || typeof value !== "object") return false;
|
|
1144
|
+
const record = value;
|
|
1145
|
+
return typeof record.fiscalCoupon === "string" && typeof record.attemptReference === "string";
|
|
1146
|
+
};
|
|
1147
|
+
const readTransaction = async (storage, key = CLISITEF_STORAGE_KEY) => {
|
|
1148
|
+
const raw = await storage.get(key);
|
|
1149
|
+
clisitefDebug("pinpad.store", "read", { raw });
|
|
1150
|
+
if (!raw) return null;
|
|
1151
|
+
try {
|
|
1152
|
+
const parsed = JSON.parse(raw);
|
|
1153
|
+
return isRecord(parsed) ? parsed : null;
|
|
1154
|
+
} catch {
|
|
1155
|
+
return null;
|
|
1156
|
+
}
|
|
1157
|
+
};
|
|
1158
|
+
const writeTransaction = async (storage, record, key = CLISITEF_STORAGE_KEY) => {
|
|
1159
|
+
clisitefDebug("pinpad.store", `write ${record.state}`, record);
|
|
1160
|
+
await storage.set(key, JSON.stringify(record));
|
|
1161
|
+
};
|
|
1162
|
+
const isOpen = (record) => {
|
|
1163
|
+
return record.state === "started" || record.state === "approved";
|
|
1164
|
+
};
|
|
1165
|
+
//#endregion
|
|
675
1166
|
//#region src/adapter.ts
|
|
676
1167
|
/** Confirmed in the act (`finishTransaction(1)`), never captured later. */
|
|
677
1168
|
const clisitefPinPadCapabilities = {
|
|
@@ -713,6 +1204,17 @@ const paid = (record) => {
|
|
|
713
1204
|
details
|
|
714
1205
|
};
|
|
715
1206
|
};
|
|
1207
|
+
/**
|
|
1208
|
+
* Silence, a deadline or an inconclusive code after the card was read, a
|
|
1209
|
+
* receipt arrived or the PIX QR was shown: the customer may have paid.
|
|
1210
|
+
*/
|
|
1211
|
+
const isAmbiguous = (outcome) => {
|
|
1212
|
+
if (outcome.status === "unresolved") return true;
|
|
1213
|
+
if (!outcome.evidence || outcome.status !== "undone") return false;
|
|
1214
|
+
const { code } = outcome;
|
|
1215
|
+
const inconclusive = typeof code === "number" && CLISITEF_INCONCLUSIVE_CODES.has(code);
|
|
1216
|
+
return outcome.aborted || inconclusive || code === CLISITEF_TIMEOUT_CODE;
|
|
1217
|
+
};
|
|
716
1218
|
const outcomeError = (outcome, attempt) => {
|
|
717
1219
|
const provider = {
|
|
718
1220
|
provider_message: outcome.message,
|
|
@@ -723,8 +1225,8 @@ const outcomeError = (outcome, attempt) => {
|
|
|
723
1225
|
...provider,
|
|
724
1226
|
provider_code: code
|
|
725
1227
|
};
|
|
1228
|
+
if (isAmbiguous(outcome)) return createPaymentError("AMBIGUOUS", extra);
|
|
726
1229
|
if (outcome.aborted) return createPaymentError("TIMEOUT", extra);
|
|
727
|
-
if (outcome.status === "unresolved") return createPaymentError("AMBIGUOUS", extra);
|
|
728
1230
|
if (outcome.code === null) {
|
|
729
1231
|
const message = outcome.message ?? void 0;
|
|
730
1232
|
return createPaymentError("PROVIDER_UNAVAILABLE", {
|
|
@@ -744,7 +1246,8 @@ const outcomeError = (outcome, attempt) => {
|
|
|
744
1246
|
const MODALITY_LABELS = {
|
|
745
1247
|
DEBIT: "Débito",
|
|
746
1248
|
CREDIT: "Crédito",
|
|
747
|
-
VOUCHER: "Voucher"
|
|
1249
|
+
VOUCHER: "Voucher",
|
|
1250
|
+
PIX: "Pix"
|
|
748
1251
|
};
|
|
749
1252
|
/** The only modality, or the one the customer picks. */
|
|
750
1253
|
const chooseModality = async (ui, modalities) => {
|
|
@@ -762,15 +1265,20 @@ const chooseModality = async (ui, modalities) => {
|
|
|
762
1265
|
});
|
|
763
1266
|
};
|
|
764
1267
|
const debugContext = (ctx) => {
|
|
765
|
-
const { attemptReference, attempt, amount, config
|
|
1268
|
+
const { attemptReference, attempt, amount, config } = ctx;
|
|
766
1269
|
return {
|
|
767
1270
|
attemptReference,
|
|
768
1271
|
attempt,
|
|
769
1272
|
amount,
|
|
770
1273
|
config,
|
|
771
|
-
deviceSettings
|
|
1274
|
+
deviceSettings: redactSecrets(ctx.deviceSettings)
|
|
772
1275
|
};
|
|
773
1276
|
};
|
|
1277
|
+
/** `review` when the customer may have paid; `approved` when the undo failed. */
|
|
1278
|
+
const closingState = (outcome) => {
|
|
1279
|
+
if (outcome.status === "unresolved") return "approved";
|
|
1280
|
+
return isAmbiguous(outcome) ? "review" : "undone";
|
|
1281
|
+
};
|
|
774
1282
|
const terminalError = (error) => {
|
|
775
1283
|
const message = error instanceof Error ? error.message : String(error);
|
|
776
1284
|
return {
|
|
@@ -799,7 +1307,7 @@ const clisitefPinPadPayments = (options = {}) => {
|
|
|
799
1307
|
const code = await client.configure(settings.configure);
|
|
800
1308
|
clisitefDebug("pinpad.adapter.configure", "result", {
|
|
801
1309
|
code,
|
|
802
|
-
settings
|
|
1310
|
+
settings: redactSecrets(settings)
|
|
803
1311
|
});
|
|
804
1312
|
if (code === 0) return null;
|
|
805
1313
|
client.invalidate();
|
|
@@ -809,7 +1317,7 @@ const clisitefPinPadPayments = (options = {}) => {
|
|
|
809
1317
|
});
|
|
810
1318
|
};
|
|
811
1319
|
/** Undoes a transaction a crash or a lost result left open. */
|
|
812
|
-
const recover = async (storage, record) => {
|
|
1320
|
+
const recover = async (storage, record, key = CLISITEF_STORAGE_KEY) => {
|
|
813
1321
|
const { fiscalCoupon, fiscalDate, time } = record;
|
|
814
1322
|
const count = client.getQttPendingTransactions;
|
|
815
1323
|
const pending = await count(fiscalDate, fiscalCoupon).catch(() => null);
|
|
@@ -857,27 +1365,52 @@ const clisitefPinPadPayments = (options = {}) => {
|
|
|
857
1365
|
};
|
|
858
1366
|
}
|
|
859
1367
|
clisitefDebug("pinpad.adapter.recover", `→ ${next.state}`, next);
|
|
860
|
-
await writeTransaction(storage, next);
|
|
1368
|
+
await writeTransaction(storage, next, key);
|
|
861
1369
|
return next;
|
|
862
1370
|
};
|
|
863
|
-
|
|
1371
|
+
const isConfigureBusy = (error) => {
|
|
1372
|
+
return error?.provider_code === "-12";
|
|
1373
|
+
};
|
|
1374
|
+
/** Drops a leftover native session so `configure` can run again. */
|
|
1375
|
+
const releaseNative = async () => {
|
|
1376
|
+
await client.abortTransaction(CLISITEF_ABORT_FLAG).catch(() => 0);
|
|
1377
|
+
await client.submitPendingMessages().catch(() => -1);
|
|
1378
|
+
await client.closePinPad().catch(() => -1);
|
|
1379
|
+
client.invalidate();
|
|
1380
|
+
};
|
|
1381
|
+
/** Configures and resolves the open records of the device, if any. */
|
|
864
1382
|
const prepare = async (ctx) => {
|
|
865
1383
|
const settings = readClisitefPinPadSettings(ctx.config, ctx.deviceSettings);
|
|
866
|
-
|
|
1384
|
+
let error = await configure(settings);
|
|
1385
|
+
if (isConfigureBusy(error)) {
|
|
1386
|
+
clisitefDebug("pinpad.adapter.release", "busy", {});
|
|
1387
|
+
await releaseNative();
|
|
1388
|
+
error = await configure(settings);
|
|
1389
|
+
}
|
|
867
1390
|
const storage = storageOf(ctx);
|
|
868
|
-
|
|
869
|
-
|
|
1391
|
+
const resolve = async (key) => {
|
|
1392
|
+
const found = await readTransaction(storage, key);
|
|
1393
|
+
if (error || !found || !isOpen(found)) return found;
|
|
1394
|
+
return recover(storage, found, key);
|
|
1395
|
+
};
|
|
1396
|
+
const record = await resolve(CLISITEF_STORAGE_KEY);
|
|
1397
|
+
const refund = await resolve(CLISITEF_REFUND_STORAGE_KEY);
|
|
870
1398
|
clisitefDebug("pinpad.adapter.prepare", "done", {
|
|
871
1399
|
error,
|
|
872
|
-
record
|
|
1400
|
+
record,
|
|
1401
|
+
refund
|
|
873
1402
|
});
|
|
874
1403
|
return {
|
|
875
1404
|
settings,
|
|
876
1405
|
error,
|
|
877
1406
|
storage,
|
|
878
|
-
record
|
|
1407
|
+
record,
|
|
1408
|
+
refund
|
|
879
1409
|
};
|
|
880
1410
|
};
|
|
1411
|
+
const busy = (records) => {
|
|
1412
|
+
return records.some((record) => record !== null && isOpen(record));
|
|
1413
|
+
};
|
|
881
1414
|
const fromRecord = (record, ctx) => {
|
|
882
1415
|
const reference = ctx.attemptReference;
|
|
883
1416
|
if (record.state === "confirmed") return paid(record);
|
|
@@ -904,7 +1437,7 @@ const clisitefPinPadPayments = (options = {}) => {
|
|
|
904
1437
|
const { settings, storage, record: previous } = prepared;
|
|
905
1438
|
if (prepared.error) return failed(empty, reference, prepared.error);
|
|
906
1439
|
if (previous?.attemptReference === reference && previous?.state === "confirmed") return paid(previous);
|
|
907
|
-
if (previous
|
|
1440
|
+
if (busy([previous, prepared.refund])) {
|
|
908
1441
|
const error = createPaymentError("TERMINAL_BUSY", { attempt: ctx.attempt });
|
|
909
1442
|
return failed(empty, reference, error);
|
|
910
1443
|
}
|
|
@@ -973,9 +1506,14 @@ const clisitefPinPadPayments = (options = {}) => {
|
|
|
973
1506
|
confirmationTimeoutMs: confirmationMs,
|
|
974
1507
|
onApproved
|
|
975
1508
|
};
|
|
1509
|
+
const t3 = {
|
|
1510
|
+
modality: chosen,
|
|
1511
|
+
purpose: "charge"
|
|
1512
|
+
};
|
|
976
1513
|
const outcome = await runClisitefTransaction({
|
|
977
1514
|
...t1,
|
|
978
|
-
...t2
|
|
1515
|
+
...t2,
|
|
1516
|
+
...t3
|
|
979
1517
|
});
|
|
980
1518
|
const fields = Object.fromEntries(outcome.fields);
|
|
981
1519
|
clisitefDebug("pinpad.adapter.charge", "outcome", {
|
|
@@ -996,17 +1534,68 @@ const clisitefPinPadPayments = (options = {}) => {
|
|
|
996
1534
|
}
|
|
997
1535
|
if (outcome.code === -1) client.invalidate();
|
|
998
1536
|
const error = outcomeError(outcome, ctx.attempt);
|
|
999
|
-
const
|
|
1000
|
-
const state = unresolved ? "approved" : "undone";
|
|
1537
|
+
const ambiguous = error.code === "AMBIGUOUS";
|
|
1001
1538
|
const closed = {
|
|
1002
1539
|
...record,
|
|
1003
|
-
state,
|
|
1540
|
+
state: closingState(outcome),
|
|
1004
1541
|
metadata,
|
|
1005
1542
|
error
|
|
1006
1543
|
};
|
|
1007
1544
|
await writeTransaction(storage, closed);
|
|
1008
1545
|
if (outcome.error) throw outcome.error;
|
|
1009
|
-
return failed(closed, reference, error,
|
|
1546
|
+
return failed(closed, reference, error, ambiguous);
|
|
1547
|
+
};
|
|
1548
|
+
const voidDeps = {
|
|
1549
|
+
client,
|
|
1550
|
+
now,
|
|
1551
|
+
timeZone: options.timeZone,
|
|
1552
|
+
confirmationTimeoutMs: confirmationMs
|
|
1553
|
+
};
|
|
1554
|
+
/** Prepares the pinpad (recovering an open charge) and voids `result`. */
|
|
1555
|
+
const refund = async (ctx, result) => {
|
|
1556
|
+
const prepared = await prepare(ctx);
|
|
1557
|
+
const { settings, storage, record } = prepared;
|
|
1558
|
+
const busy = record && isOpen(record);
|
|
1559
|
+
const busyError = createPaymentError("TERMINAL_BUSY");
|
|
1560
|
+
const unavailable = prepared.error ?? (busy ? busyError : null);
|
|
1561
|
+
const v1 = {
|
|
1562
|
+
ui: ctx.ui,
|
|
1563
|
+
signal: ctx.signal,
|
|
1564
|
+
storage,
|
|
1565
|
+
settings
|
|
1566
|
+
};
|
|
1567
|
+
const v2 = {
|
|
1568
|
+
attempt: ctx.attempt,
|
|
1569
|
+
unavailable
|
|
1570
|
+
};
|
|
1571
|
+
return runClisitefVoid(voidDeps, {
|
|
1572
|
+
...v1,
|
|
1573
|
+
...v2
|
|
1574
|
+
}, result);
|
|
1575
|
+
};
|
|
1576
|
+
const retryReversals = async (ctx) => {
|
|
1577
|
+
const settings = readClisitefPinPadSettings(ctx.config, ctx.deviceSettings);
|
|
1578
|
+
const error = await configure(settings);
|
|
1579
|
+
if (error) throw new Error(error.message);
|
|
1580
|
+
const storage = storageOf(ctx);
|
|
1581
|
+
const pending = await readPendingReversals(storage);
|
|
1582
|
+
let refunded = 0;
|
|
1583
|
+
for (const entry of pending) {
|
|
1584
|
+
const v1 = {
|
|
1585
|
+
ui: ctx.ui,
|
|
1586
|
+
signal: ctx.signal,
|
|
1587
|
+
storage,
|
|
1588
|
+
settings
|
|
1589
|
+
};
|
|
1590
|
+
const v2 = { attempt: entry.attempts + 1 };
|
|
1591
|
+
const ctx1 = {
|
|
1592
|
+
...v1,
|
|
1593
|
+
...v2
|
|
1594
|
+
};
|
|
1595
|
+
if ((await runClisitefVoid(voidDeps, ctx1, entry.result)).status === "REFUNDED") refunded += 1;
|
|
1596
|
+
}
|
|
1597
|
+
const left = (await readPendingReversals(storage)).length;
|
|
1598
|
+
return `Voids completed: ${refunded}, pending: ${left}`;
|
|
1010
1599
|
};
|
|
1011
1600
|
const query = async (ctx, result) => {
|
|
1012
1601
|
const reference = result.metadata.attempt_reference;
|
|
@@ -1046,9 +1635,10 @@ const clisitefPinPadPayments = (options = {}) => {
|
|
|
1046
1635
|
const operation = (run, name = "terminal") => {
|
|
1047
1636
|
return (ctx) => {
|
|
1048
1637
|
const scope = `pinpad.terminal.${name}`;
|
|
1638
|
+
const deviceSettings = redactSecrets(ctx.deviceSettings);
|
|
1049
1639
|
const input = {
|
|
1050
1640
|
config: ctx.config,
|
|
1051
|
-
deviceSettings
|
|
1641
|
+
deviceSettings
|
|
1052
1642
|
};
|
|
1053
1643
|
const traced = () => clisitefDebugCall(scope, input, task);
|
|
1054
1644
|
const task = async () => {
|
|
@@ -1078,22 +1668,30 @@ const clisitefPinPadPayments = (options = {}) => {
|
|
|
1078
1668
|
return `CliSiTef ${await client.getVersion()}`;
|
|
1079
1669
|
}, "test"),
|
|
1080
1670
|
status: operation(async () => client.getHealth() ?? {}, "status"),
|
|
1081
|
-
extra: [
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1671
|
+
extra: [
|
|
1672
|
+
{
|
|
1673
|
+
key: "submit_pending_messages",
|
|
1674
|
+
label: "Send pending messages",
|
|
1675
|
+
run: operation(async () => {
|
|
1676
|
+
return `Pending messages sent (${await client.submitPendingMessages()})`;
|
|
1677
|
+
}, "submit_pending_messages")
|
|
1678
|
+
},
|
|
1679
|
+
{
|
|
1680
|
+
key: "retry_pending_reversals",
|
|
1681
|
+
label: "Retry pending voids",
|
|
1682
|
+
run: operation(retryReversals, "retry_pending_reversals")
|
|
1683
|
+
},
|
|
1684
|
+
{
|
|
1685
|
+
key: "release_pinpad",
|
|
1686
|
+
label: "Release pinpad",
|
|
1687
|
+
run: operation(async () => {
|
|
1688
|
+
await client.closePinPad();
|
|
1689
|
+
await client.disconnectSitef();
|
|
1690
|
+
client.invalidate();
|
|
1691
|
+
return "Pinpad released";
|
|
1692
|
+
}, "release_pinpad")
|
|
1693
|
+
}
|
|
1694
|
+
]
|
|
1097
1695
|
};
|
|
1098
1696
|
return {
|
|
1099
1697
|
provider: "clisitef",
|
|
@@ -1123,9 +1721,17 @@ const clisitefPinPadPayments = (options = {}) => {
|
|
|
1123
1721
|
const run = () => cancel(ctx, result);
|
|
1124
1722
|
return lock(() => clisitefDebugCall("pinpad.cancel", input, run));
|
|
1125
1723
|
},
|
|
1126
|
-
|
|
1724
|
+
refund: (ctx, result) => {
|
|
1725
|
+
const input = {
|
|
1726
|
+
ctx: debugContext(ctx),
|
|
1727
|
+
result
|
|
1728
|
+
};
|
|
1729
|
+
const run = () => refund(ctx, result);
|
|
1730
|
+
return lock(() => clisitefDebugCall("pinpad.refund", input, run));
|
|
1731
|
+
},
|
|
1732
|
+
reversalFor: (result) => result.status === "PAID" ? "refund" : "cancel",
|
|
1127
1733
|
terminal
|
|
1128
1734
|
};
|
|
1129
1735
|
};
|
|
1130
1736
|
//#endregion
|
|
1131
|
-
export { CLISITEF_CANCEL_CODES, CLISITEF_CAPTURE_FIELDS, CLISITEF_COMMAND, CLISITEF_CONFIRMATION_TIMEOUT_MS, CLISITEF_DEFAULT_OPERATOR, CLISITEF_FIELD, CLISITEF_INCONCLUSIVE_CODES, CLISITEF_NO, CLISITEF_PAYMENT_TIMEOUT_MS, CLISITEF_PINPAD_TYPES, CLISITEF_STORAGE_KEY, CLISITEF_YES, clisitefDetails, clisitefLast4, clisitefMetadata, clisitefPinPadCapabilities, clisitefPinPadDeviceFields, clisitefPinPadErrors, clisitefPinPadPayments, isOpen, readClisitefPinPadSettings, readTransaction, runClisitefTransaction, writeTransaction };
|
|
1737
|
+
export { CLISITEF_CANCEL_CODES, CLISITEF_CAPTURE_FIELDS, CLISITEF_COMMAND, CLISITEF_CONFIRMATION_TIMEOUT_MS, CLISITEF_DEFAULT_OPERATOR, CLISITEF_FIELD, CLISITEF_INCONCLUSIVE_CODES, CLISITEF_MODALITIES, CLISITEF_NO, CLISITEF_PAYMENT_TIMEOUT_MS, CLISITEF_PINPAD_TYPES, CLISITEF_REFUNDS_KEY, CLISITEF_REFUND_STORAGE_KEY, CLISITEF_REVERSALS_KEY, CLISITEF_STORAGE_KEY, CLISITEF_VOID_FIELD, CLISITEF_YES, clisitefDetails, clisitefLast4, clisitefMetadata, clisitefModalityOf, clisitefPinPadCapabilities, clisitefPinPadDeviceFields, clisitefPinPadErrors, clisitefPinPadPayments, clisitefVoidMetadata, dropReversal, isClisitefModality, isOpen, queueReversal, readClisitefPinPadSettings, readPendingReversals, readRefund, readTransaction, redactSecrets, resolveClisitefMenu, runClisitefTransaction, runClisitefVoid, writeRefund, writeTransaction };
|