@fire-spark/payments-clisitef-pinpad-rn 0.0.0-stage → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +207 -0
- package/dist/index.js +977 -0
- package/package.json +58 -3
- package/README.md +0 -3
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
import { DeviceField, PaymentAdapter, PaymentAdapterCapabilities, PaymentErrorEntry, PaymentMetadata, PaymentStorage, PaymentUI, StandardPaymentError } from "@fire-spark/payments";
|
|
2
|
+
import { CLISITEF_MODALITY, ClisitefClient, ClisitefConfigureParams, ClisitefFinishParams, ClisitefStartParams } from "@fire-spark/payments-clisitef-client";
|
|
3
|
+
import { TenderDetails } from "@fire-spark/types";
|
|
4
|
+
//#region src/adapter.d.ts
|
|
5
|
+
/** Confirmed in the act (`finishTransaction(1)`), never captured later. */
|
|
6
|
+
declare const clisitefPinPadCapabilities: PaymentAdapterCapabilities;
|
|
7
|
+
type ClisitefPinPadOptions = {
|
|
8
|
+
/** Defaults to the shared client bound to the native module. */
|
|
9
|
+
client?: ClisitefClient;
|
|
10
|
+
/**
|
|
11
|
+
* Device-wide storage for the pending-transaction record. Defaults to
|
|
12
|
+
* `ctx.storage`, which is scoped per payment: a transaction left open by
|
|
13
|
+
* the DEBIT payment is then not seen by the CREDIT one.
|
|
14
|
+
*/
|
|
15
|
+
storage?: PaymentStorage;
|
|
16
|
+
/** Store time zone for the fiscal date; defaults to `America/Sao_Paulo`. */
|
|
17
|
+
timeZone?: string;
|
|
18
|
+
now?: () => Date;
|
|
19
|
+
confirmationTimeoutMs?: number;
|
|
20
|
+
};
|
|
21
|
+
/**
|
|
22
|
+
* CliSiTef (Software Express) on the kiosk pinpad, Brazil. One adapter per
|
|
23
|
+
* modality payment (DEBIT / CREDIT / VOUCHER in `payments.config`). The
|
|
24
|
+
* pinpad waits on the customer: give the payment a `runtime.timeout_ms`
|
|
25
|
+
* well above the 30 s default (e.g. 90 000).
|
|
26
|
+
*/
|
|
27
|
+
declare const clisitefPinPadPayments: (options?: ClisitefPinPadOptions) => PaymentAdapter;
|
|
28
|
+
//#endregion
|
|
29
|
+
//#region src/constants.d.ts
|
|
30
|
+
/**
|
|
31
|
+
* `onData` commands handled by the adapter (Fiserv "Tabela de códigos de
|
|
32
|
+
* comando"). Not handled, so they abort the transaction: 29 (silent field),
|
|
33
|
+
* 31 (check), 35 (barcode), 42 (identified menu, only with
|
|
34
|
+
* `ItemMenuIdentificado`) and 50/51 (QR, PIX only).
|
|
35
|
+
*/
|
|
36
|
+
declare const CLISITEF_COMMAND: {
|
|
37
|
+
readonly STORE_FIELD: 0;
|
|
38
|
+
readonly MESSAGE_OPERATOR: 1;
|
|
39
|
+
readonly MESSAGE_CUSTOMER: 2;
|
|
40
|
+
readonly MESSAGE_BOTH: 3;
|
|
41
|
+
readonly MENU_TITLE: 4;
|
|
42
|
+
readonly CLEAR_OPERATOR: 11;
|
|
43
|
+
readonly CLEAR_CUSTOMER: 12;
|
|
44
|
+
readonly CLEAR_BOTH: 13;
|
|
45
|
+
readonly CLEAR_MENU_TITLE: 14;
|
|
46
|
+
readonly HEADER: 15;
|
|
47
|
+
readonly CLEAR_HEADER: 16;
|
|
48
|
+
readonly CONFIRM: 20;
|
|
49
|
+
readonly MENU: 21;
|
|
50
|
+
readonly PRESS_KEY: 22;
|
|
51
|
+
readonly CHECK_ABORT: 23;
|
|
52
|
+
readonly READ_FIELD: 30;
|
|
53
|
+
readonly READ_AMOUNT: 34;
|
|
54
|
+
readonly READ_MASKED: 41;
|
|
55
|
+
readonly FOOTER: 52;
|
|
56
|
+
};
|
|
57
|
+
/** `continueTransaction` answers to `CONFIRM` (20). */
|
|
58
|
+
declare const CLISITEF_YES = "0";
|
|
59
|
+
declare const CLISITEF_NO = "1";
|
|
60
|
+
/** `TipoCampo` ids stored with `STORE_FIELD` (Fiserv "Tabela TipoCampo"). */
|
|
61
|
+
declare const CLISITEF_FIELD: {
|
|
62
|
+
readonly TRANSACTION_DATE: 105;
|
|
63
|
+
readonly CUSTOMER_RECEIPT: 121;
|
|
64
|
+
readonly MERCHANT_RECEIPT: 122;
|
|
65
|
+
readonly INSTITUTION_CODE: 131;
|
|
66
|
+
readonly CARD_TYPE_CODE: 132;
|
|
67
|
+
readonly SITEF_NSU: 133;
|
|
68
|
+
readonly HOST_NSU: 134;
|
|
69
|
+
readonly AUTHORIZATION_CODE: 135;
|
|
70
|
+
readonly CARD_BIN: 136;
|
|
71
|
+
readonly INSTITUTION_NAME: 156;
|
|
72
|
+
readonly LAST4: 1190;
|
|
73
|
+
readonly ENTRY_MODE: 2090;
|
|
74
|
+
};
|
|
75
|
+
/** Fields that prove the card was read: an error after them is ambiguous. */
|
|
76
|
+
declare const CLISITEF_CAPTURE_FIELDS: ReadonlySet<number>;
|
|
77
|
+
/** Operator, pinpad or automation cancelled (ficha §11). */
|
|
78
|
+
declare const CLISITEF_CANCEL_CODES: ReadonlySet<number>;
|
|
79
|
+
/** Inconclusive when the card was already read (ficha §4). */
|
|
80
|
+
declare const CLISITEF_INCONCLUSIVE_CODES: ReadonlySet<number>;
|
|
81
|
+
/** Approved (`stage 1`) without `stage 2`: undo (legacy `CONFIRMATION_TIMEOUT_MS`). */
|
|
82
|
+
declare const CLISITEF_CONFIRMATION_TIMEOUT_MS = 60000;
|
|
83
|
+
/** Legacy operator when the merchant config has none. */
|
|
84
|
+
declare const CLISITEF_DEFAULT_OPERATOR = "KIOSKO_BR";
|
|
85
|
+
/** Device-wide record of the last transaction, to undo it after a crash. */
|
|
86
|
+
declare const CLISITEF_STORAGE_KEY = "clisitef:transaction:v1";
|
|
87
|
+
//#endregion
|
|
88
|
+
//#region src/errors.d.ts
|
|
89
|
+
/**
|
|
90
|
+
* Ficha §11 (Fiserv "Tabela de códigos de retorno"); other positive codes
|
|
91
|
+
* (authorizer) fall back to `CARD_DECLINED`, other negatives to `UNKNOWN`.
|
|
92
|
+
*/
|
|
93
|
+
declare const clisitefPinPadErrors: Readonly<Record<string, PaymentErrorEntry>> & {
|
|
94
|
+
"-1": PaymentErrorEntry;
|
|
95
|
+
"-2": PaymentErrorEntry;
|
|
96
|
+
"-3": PaymentErrorEntry;
|
|
97
|
+
"-4": PaymentErrorEntry;
|
|
98
|
+
"-5": PaymentErrorEntry;
|
|
99
|
+
"-6": PaymentErrorEntry;
|
|
100
|
+
"-8": PaymentErrorEntry;
|
|
101
|
+
"-9": PaymentErrorEntry;
|
|
102
|
+
"-10": PaymentErrorEntry;
|
|
103
|
+
"-12": PaymentErrorEntry;
|
|
104
|
+
"-13": PaymentErrorEntry;
|
|
105
|
+
"-15": PaymentErrorEntry;
|
|
106
|
+
"-20": PaymentErrorEntry;
|
|
107
|
+
"-21": PaymentErrorEntry;
|
|
108
|
+
"-24": PaymentErrorEntry;
|
|
109
|
+
"-30": PaymentErrorEntry;
|
|
110
|
+
"-40": PaymentErrorEntry;
|
|
111
|
+
"-41": PaymentErrorEntry;
|
|
112
|
+
"-43": PaymentErrorEntry;
|
|
113
|
+
"-50": PaymentErrorEntry;
|
|
114
|
+
"-100": PaymentErrorEntry;
|
|
115
|
+
CLISITEF_TIMEOUT: PaymentErrorEntry;
|
|
116
|
+
};
|
|
117
|
+
//#endregion
|
|
118
|
+
//#region src/settings.d.ts
|
|
119
|
+
declare const CLISITEF_PINPAD_TYPES: readonly ["ANDROID_USB", "ANDROID_BT", "ANDROID_AUTO"];
|
|
120
|
+
/** Ficha §6: filled per kiosk in the device settings. */
|
|
121
|
+
declare const clisitefPinPadDeviceFields: DeviceField[];
|
|
122
|
+
type ClisitefModality = keyof typeof CLISITEF_MODALITY;
|
|
123
|
+
type ClisitefPinPadSettings = {
|
|
124
|
+
configure: ClisitefConfigureParams;
|
|
125
|
+
modality: Exclude<ClisitefModality, "PIX">;
|
|
126
|
+
operator: string;
|
|
127
|
+
restrictions: string;
|
|
128
|
+
terminalNumber: string;
|
|
129
|
+
};
|
|
130
|
+
/** `payments.config` (merchant) + device settings; throws when incomplete. */
|
|
131
|
+
declare const readClisitefPinPadSettings: (config: Record<string, unknown>, device: Record<string, unknown>) => ClisitefPinPadSettings;
|
|
132
|
+
//#endregion
|
|
133
|
+
//#region src/metadata.d.ts
|
|
134
|
+
type ClisitefFields = ReadonlyMap<number, string>;
|
|
135
|
+
/** Last 4 digits of the masked PAN printed on the receipt. */
|
|
136
|
+
declare const clisitefLast4: (receipt: string | null) => string | null;
|
|
137
|
+
type MetadataInput = {
|
|
138
|
+
attemptReference: string;
|
|
139
|
+
fields: ClisitefFields;
|
|
140
|
+
modality: ClisitefModality;
|
|
141
|
+
terminalNumber: string;
|
|
142
|
+
fiscalCoupon: string;
|
|
143
|
+
fiscalDate: string;
|
|
144
|
+
};
|
|
145
|
+
declare const clisitefMetadata: (input: MetadataInput) => PaymentMetadata;
|
|
146
|
+
/** PIN_PAD tender details; `null` without brand and last 4 (schema minimum). */
|
|
147
|
+
declare const clisitefDetails: (metadata: PaymentMetadata) => TenderDetails | null;
|
|
148
|
+
//#endregion
|
|
149
|
+
//#region src/store.d.ts
|
|
150
|
+
/**
|
|
151
|
+
* `started`: `startTransaction` called. `approved`: `finishTransaction(1)`
|
|
152
|
+
* called, no `stage 2` yet. `confirmed` / `undone`: closed either way.
|
|
153
|
+
* `review`: approved, then lost, and no longer pending in CliSiTef: it may
|
|
154
|
+
* have been charged.
|
|
155
|
+
*/
|
|
156
|
+
type ClisitefTransactionState = "started" | "approved" | "confirmed" | "undone" | "review";
|
|
157
|
+
type ClisitefTransactionRecord = {
|
|
158
|
+
attemptReference: string;
|
|
159
|
+
fiscalCoupon: string;
|
|
160
|
+
fiscalDate: string;
|
|
161
|
+
time: string;
|
|
162
|
+
startedAt: number;
|
|
163
|
+
state: ClisitefTransactionState;
|
|
164
|
+
amount: number;
|
|
165
|
+
metadata?: PaymentMetadata;
|
|
166
|
+
details?: TenderDetails | null;
|
|
167
|
+
error?: StandardPaymentError;
|
|
168
|
+
};
|
|
169
|
+
declare const readTransaction: (storage: PaymentStorage) => Promise<ClisitefTransactionRecord | null>;
|
|
170
|
+
declare const writeTransaction: (storage: PaymentStorage, record: ClisitefTransactionRecord) => Promise<void>;
|
|
171
|
+
declare const isOpen: (record: ClisitefTransactionRecord) => boolean;
|
|
172
|
+
//#endregion
|
|
173
|
+
//#region src/transaction.d.ts
|
|
174
|
+
/**
|
|
175
|
+
* `confirmed`: `stage 2` closed with 0. `declined`: failed before the card
|
|
176
|
+
* was charged. `undone`: `finishTransaction(0)` sent after a capture or an
|
|
177
|
+
* approval. `unresolved`: the undo itself failed; needs review.
|
|
178
|
+
*/
|
|
179
|
+
type ClisitefOutcomeStatus = "confirmed" | "declined" | "undone" | "unresolved";
|
|
180
|
+
type ClisitefOutcome = {
|
|
181
|
+
status: ClisitefOutcomeStatus;
|
|
182
|
+
/** Provider code: `resultCode`, a client error code, or `null`. */
|
|
183
|
+
code: number | string | null;
|
|
184
|
+
message: string | null;
|
|
185
|
+
fields: ClisitefFields;
|
|
186
|
+
aborted: boolean;
|
|
187
|
+
/** Error thrown by `ctx.ui` (e.g. the customer did not answer). */
|
|
188
|
+
error?: unknown;
|
|
189
|
+
};
|
|
190
|
+
type ClisitefTransactionInput = {
|
|
191
|
+
client: ClisitefClient;
|
|
192
|
+
ui: PaymentUI;
|
|
193
|
+
signal: AbortSignal;
|
|
194
|
+
start: ClisitefStartParams;
|
|
195
|
+
finish: Omit<ClisitefFinishParams, "confirm">;
|
|
196
|
+
confirmationTimeoutMs: number;
|
|
197
|
+
/** Runs before `finishTransaction(1)`, to persist the approval. */
|
|
198
|
+
onApproved: (fields: ClisitefFields) => Promise<void>;
|
|
199
|
+
};
|
|
200
|
+
/**
|
|
201
|
+
* One CliSiTef transaction: `startTransaction`, the `onData` loop answered
|
|
202
|
+
* through `ui`, `finishTransaction(1)` on approval (`stage 1` = 0) and
|
|
203
|
+
* success only when `stage 2` closes with 0. Never rejects.
|
|
204
|
+
*/
|
|
205
|
+
declare const runClisitefTransaction: (input: ClisitefTransactionInput) => Promise<ClisitefOutcome>;
|
|
206
|
+
//#endregion
|
|
207
|
+
export { CLISITEF_CANCEL_CODES, CLISITEF_CAPTURE_FIELDS, CLISITEF_COMMAND, CLISITEF_CONFIRMATION_TIMEOUT_MS, CLISITEF_DEFAULT_OPERATOR, CLISITEF_FIELD, CLISITEF_INCONCLUSIVE_CODES, CLISITEF_NO, CLISITEF_PINPAD_TYPES, CLISITEF_STORAGE_KEY, CLISITEF_YES, ClisitefFields, ClisitefModality, ClisitefOutcome, ClisitefOutcomeStatus, ClisitefPinPadOptions, ClisitefPinPadSettings, ClisitefTransactionInput, ClisitefTransactionRecord, ClisitefTransactionState, clisitefDetails, clisitefLast4, clisitefMetadata, clisitefPinPadCapabilities, clisitefPinPadDeviceFields, clisitefPinPadErrors, clisitefPinPadPayments, isOpen, readClisitefPinPadSettings, readTransaction, runClisitefTransaction, writeTransaction };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,977 @@
|
|
|
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, clisitefFiscalDateTime, formatClisitefAmount, parseClisitefMenu } from "@fire-spark/payments-clisitef-client";
|
|
3
|
+
//#region src/constants.ts
|
|
4
|
+
/**
|
|
5
|
+
* `onData` commands handled by the adapter (Fiserv "Tabela de códigos de
|
|
6
|
+
* comando"). Not handled, so they abort the transaction: 29 (silent field),
|
|
7
|
+
* 31 (check), 35 (barcode), 42 (identified menu, only with
|
|
8
|
+
* `ItemMenuIdentificado`) and 50/51 (QR, PIX only).
|
|
9
|
+
*/
|
|
10
|
+
const CLISITEF_COMMAND = {
|
|
11
|
+
STORE_FIELD: 0,
|
|
12
|
+
MESSAGE_OPERATOR: 1,
|
|
13
|
+
MESSAGE_CUSTOMER: 2,
|
|
14
|
+
MESSAGE_BOTH: 3,
|
|
15
|
+
MENU_TITLE: 4,
|
|
16
|
+
CLEAR_OPERATOR: 11,
|
|
17
|
+
CLEAR_CUSTOMER: 12,
|
|
18
|
+
CLEAR_BOTH: 13,
|
|
19
|
+
CLEAR_MENU_TITLE: 14,
|
|
20
|
+
HEADER: 15,
|
|
21
|
+
CLEAR_HEADER: 16,
|
|
22
|
+
CONFIRM: 20,
|
|
23
|
+
MENU: 21,
|
|
24
|
+
PRESS_KEY: 22,
|
|
25
|
+
CHECK_ABORT: 23,
|
|
26
|
+
READ_FIELD: 30,
|
|
27
|
+
READ_AMOUNT: 34,
|
|
28
|
+
READ_MASKED: 41,
|
|
29
|
+
FOOTER: 52
|
|
30
|
+
};
|
|
31
|
+
/** `continueTransaction` answers to `CONFIRM` (20). */
|
|
32
|
+
const CLISITEF_YES = "0";
|
|
33
|
+
const CLISITEF_NO = "1";
|
|
34
|
+
/** `TipoCampo` ids stored with `STORE_FIELD` (Fiserv "Tabela TipoCampo"). */
|
|
35
|
+
const CLISITEF_FIELD = {
|
|
36
|
+
TRANSACTION_DATE: 105,
|
|
37
|
+
CUSTOMER_RECEIPT: 121,
|
|
38
|
+
MERCHANT_RECEIPT: 122,
|
|
39
|
+
INSTITUTION_CODE: 131,
|
|
40
|
+
CARD_TYPE_CODE: 132,
|
|
41
|
+
SITEF_NSU: 133,
|
|
42
|
+
HOST_NSU: 134,
|
|
43
|
+
AUTHORIZATION_CODE: 135,
|
|
44
|
+
CARD_BIN: 136,
|
|
45
|
+
INSTITUTION_NAME: 156,
|
|
46
|
+
LAST4: 1190,
|
|
47
|
+
ENTRY_MODE: 2090
|
|
48
|
+
};
|
|
49
|
+
/** Fields that prove the card was read: an error after them is ambiguous. */
|
|
50
|
+
const CLISITEF_CAPTURE_FIELDS = /* @__PURE__ */ new Set([
|
|
51
|
+
CLISITEF_FIELD.CUSTOMER_RECEIPT,
|
|
52
|
+
CLISITEF_FIELD.MERCHANT_RECEIPT,
|
|
53
|
+
CLISITEF_FIELD.CARD_TYPE_CODE,
|
|
54
|
+
CLISITEF_FIELD.HOST_NSU,
|
|
55
|
+
CLISITEF_FIELD.AUTHORIZATION_CODE,
|
|
56
|
+
CLISITEF_FIELD.CARD_BIN,
|
|
57
|
+
CLISITEF_FIELD.LAST4,
|
|
58
|
+
CLISITEF_FIELD.ENTRY_MODE
|
|
59
|
+
]);
|
|
60
|
+
/** Operator, pinpad or automation cancelled (ficha §11). */
|
|
61
|
+
const CLISITEF_CANCEL_CODES = /* @__PURE__ */ new Set([
|
|
62
|
+
-2,
|
|
63
|
+
-6,
|
|
64
|
+
-15
|
|
65
|
+
]);
|
|
66
|
+
/** Inconclusive when the card was already read (ficha §4). */
|
|
67
|
+
const CLISITEF_INCONCLUSIVE_CODES = /* @__PURE__ */ new Set([
|
|
68
|
+
-5,
|
|
69
|
+
-43,
|
|
70
|
+
-100
|
|
71
|
+
]);
|
|
72
|
+
/** Approved (`stage 1`) without `stage 2`: undo (legacy `CONFIRMATION_TIMEOUT_MS`). */
|
|
73
|
+
const CLISITEF_CONFIRMATION_TIMEOUT_MS = 6e4;
|
|
74
|
+
/** Legacy operator when the merchant config has none. */
|
|
75
|
+
const CLISITEF_DEFAULT_OPERATOR = "KIOSKO_BR";
|
|
76
|
+
/** Device-wide record of the last transaction, to undo it after a crash. */
|
|
77
|
+
const CLISITEF_STORAGE_KEY = "clisitef:transaction:v1";
|
|
78
|
+
//#endregion
|
|
79
|
+
//#region src/errors.ts
|
|
80
|
+
const source = "docs/payments/clisitef/pinpad.md#11-errores";
|
|
81
|
+
const entry = (code, extra = {}) => {
|
|
82
|
+
return {
|
|
83
|
+
code,
|
|
84
|
+
source,
|
|
85
|
+
...extra
|
|
86
|
+
};
|
|
87
|
+
};
|
|
88
|
+
const invalid = (message) => entry("INVALID_REQUEST", { message });
|
|
89
|
+
/**
|
|
90
|
+
* Ficha §11 (Fiserv "Tabela de códigos de retorno"); other positive codes
|
|
91
|
+
* (authorizer) fall back to `CARD_DECLINED`, other negatives to `UNKNOWN`.
|
|
92
|
+
*/
|
|
93
|
+
const clisitefPinPadErrors = definePaymentErrors({
|
|
94
|
+
"-1": entry("TERMINAL_NEEDS_INIT", { retryable: true }),
|
|
95
|
+
"-2": entry("CANCELLED_BY_USER"),
|
|
96
|
+
"-3": invalid("Invalid CliSiTef function or modality"),
|
|
97
|
+
"-4": entry("TERMINAL_UNAVAILABLE", { message: "Out of memory" }),
|
|
98
|
+
"-5": entry("PROVIDER_UNAVAILABLE"),
|
|
99
|
+
"-6": entry("CANCELLED_BY_USER"),
|
|
100
|
+
"-8": entry("TERMINAL_NEEDS_INIT", { message: "CliSiTef outdated" }),
|
|
101
|
+
"-9": invalid("CliSiTef iterative flow not started"),
|
|
102
|
+
"-10": invalid("Missing mandatory CliSiTef parameter"),
|
|
103
|
+
"-12": entry("TERMINAL_BUSY"),
|
|
104
|
+
"-13": entry("UNKNOWN", { message: "Fiscal document not found" }),
|
|
105
|
+
"-15": entry("CANCELLED_BY_USER"),
|
|
106
|
+
"-20": invalid("Invalid CliSiTef parameter"),
|
|
107
|
+
"-21": invalid("Prohibited word in pinpad collection"),
|
|
108
|
+
"-24": invalid("Digital wallet not enabled"),
|
|
109
|
+
"-30": entry("TERMINAL_UNAVAILABLE", { message: "File access error" }),
|
|
110
|
+
"-40": entry("CARD_DECLINED", { message: "Declined by the SiTef server" }),
|
|
111
|
+
"-41": invalid("Invalid data"),
|
|
112
|
+
"-43": entry("TERMINAL_UNAVAILABLE", { message: "Pinpad routine failed" }),
|
|
113
|
+
"-50": entry("FRAUD_SUSPECTED", { message: "Unsecured transaction" }),
|
|
114
|
+
"-100": entry("UNKNOWN", { message: "CliSiTef internal error" }),
|
|
115
|
+
[CLISITEF_TIMEOUT_CODE]: entry("TIMEOUT")
|
|
116
|
+
});
|
|
117
|
+
//#endregion
|
|
118
|
+
//#region src/metadata.ts
|
|
119
|
+
const MASKED_PAN = /\d{0,6}[*Xx]{4,}\s*(\d{4})(?!\d)/;
|
|
120
|
+
const field = (fields, id) => {
|
|
121
|
+
const value = fields.get(id)?.trim();
|
|
122
|
+
return value ? value : null;
|
|
123
|
+
};
|
|
124
|
+
/** Last 4 digits of the masked PAN printed on the receipt. */
|
|
125
|
+
const clisitefLast4 = (receipt) => {
|
|
126
|
+
if (!receipt) return null;
|
|
127
|
+
return MASKED_PAN.exec(receipt)?.[1] ?? null;
|
|
128
|
+
};
|
|
129
|
+
/**
|
|
130
|
+
* Field 132. Fiserv documents it as "a table to be provided"; these codes
|
|
131
|
+
* come from the legacy production catalog (ficha §8).
|
|
132
|
+
*/
|
|
133
|
+
const BRANDS = {
|
|
134
|
+
"00001": "VISA",
|
|
135
|
+
"00002": "MASTERCARD",
|
|
136
|
+
"00004": "AMEX",
|
|
137
|
+
"00011": "JCB",
|
|
138
|
+
"10014": "DISCOVER",
|
|
139
|
+
"20001": "MAESTRO",
|
|
140
|
+
"20002": "VISA_ELECTRON",
|
|
141
|
+
"20137": "TELENET"
|
|
142
|
+
};
|
|
143
|
+
/** Field 2090 (Fiserv "Tabela TipoCampo"). */
|
|
144
|
+
const ENTRY_MODES = {
|
|
145
|
+
"00": "SWIPE",
|
|
146
|
+
"03": "CHIP",
|
|
147
|
+
"05": "CONTACTLESS",
|
|
148
|
+
"06": "CONTACTLESS",
|
|
149
|
+
"99": "MANUAL"
|
|
150
|
+
};
|
|
151
|
+
const clisitefBrand = (fields) => {
|
|
152
|
+
const code = field(fields, CLISITEF_FIELD.CARD_TYPE_CODE);
|
|
153
|
+
if (!code || code === "00000") return null;
|
|
154
|
+
return BRANDS[code] ?? code;
|
|
155
|
+
};
|
|
156
|
+
const CARD_TYPES = {
|
|
157
|
+
DEBIT: "DEBIT",
|
|
158
|
+
CREDIT: "CREDIT",
|
|
159
|
+
VOUCHER: "PREPAID",
|
|
160
|
+
PIX: null
|
|
161
|
+
};
|
|
162
|
+
const clisitefMetadata = (input) => {
|
|
163
|
+
const { fields } = input;
|
|
164
|
+
const customer = field(fields, CLISITEF_FIELD.CUSTOMER_RECEIPT);
|
|
165
|
+
const merchant = field(fields, CLISITEF_FIELD.MERCHANT_RECEIPT);
|
|
166
|
+
const brand = clisitefBrand(fields);
|
|
167
|
+
const last4 = field(fields, CLISITEF_FIELD.LAST4)?.slice(-4) ?? null;
|
|
168
|
+
const entry = field(fields, CLISITEF_FIELD.ENTRY_MODE);
|
|
169
|
+
const institution = field(fields, CLISITEF_FIELD.INSTITUTION_NAME);
|
|
170
|
+
const p1 = {
|
|
171
|
+
provider_transaction_id: field(fields, CLISITEF_FIELD.SITEF_NSU),
|
|
172
|
+
authorization_code: field(fields, CLISITEF_FIELD.AUTHORIZATION_CODE),
|
|
173
|
+
reference: field(fields, CLISITEF_FIELD.HOST_NSU)
|
|
174
|
+
};
|
|
175
|
+
const p2 = {
|
|
176
|
+
card_type: CARD_TYPES[input.modality] ?? null,
|
|
177
|
+
brand
|
|
178
|
+
};
|
|
179
|
+
const p3 = {
|
|
180
|
+
last4: last4 ?? clisitefLast4(customer ?? merchant),
|
|
181
|
+
entry_mode: entry ? ENTRY_MODES[entry] ?? null : null
|
|
182
|
+
};
|
|
183
|
+
const p4 = { acquirer: institution ?? field(fields, CLISITEF_FIELD.INSTITUTION_CODE) };
|
|
184
|
+
const p5 = { terminal_id: input.terminalNumber };
|
|
185
|
+
const p6 = { ticket_number: input.fiscalCoupon };
|
|
186
|
+
const p7 = { transaction_date: input.fiscalDate };
|
|
187
|
+
const payment = {
|
|
188
|
+
...p1,
|
|
189
|
+
...p2,
|
|
190
|
+
...p3,
|
|
191
|
+
...p4,
|
|
192
|
+
...p5,
|
|
193
|
+
...p6,
|
|
194
|
+
...p7
|
|
195
|
+
};
|
|
196
|
+
const receipt = customer !== null || merchant !== null ? {
|
|
197
|
+
customer,
|
|
198
|
+
merchant,
|
|
199
|
+
format: "text"
|
|
200
|
+
} : void 0;
|
|
201
|
+
return {
|
|
202
|
+
attempt_reference: input.attemptReference,
|
|
203
|
+
payment,
|
|
204
|
+
receipt
|
|
205
|
+
};
|
|
206
|
+
};
|
|
207
|
+
/** PIN_PAD tender details; `null` without brand and last 4 (schema minimum). */
|
|
208
|
+
const clisitefDetails = (metadata) => {
|
|
209
|
+
const payment = metadata.payment;
|
|
210
|
+
const brand = payment?.brand?.slice(0, 32);
|
|
211
|
+
const last4 = payment?.last4;
|
|
212
|
+
const terminal = payment?.terminal_id;
|
|
213
|
+
if (!brand || !last4 || !terminal) return null;
|
|
214
|
+
return {
|
|
215
|
+
method: "PIN_PAD",
|
|
216
|
+
brand,
|
|
217
|
+
last4,
|
|
218
|
+
auth_code: payment.authorization_code ?? void 0,
|
|
219
|
+
terminal_id: terminal
|
|
220
|
+
};
|
|
221
|
+
};
|
|
222
|
+
//#endregion
|
|
223
|
+
//#region src/settings.ts
|
|
224
|
+
const CLISITEF_PINPAD_TYPES = [
|
|
225
|
+
"ANDROID_USB",
|
|
226
|
+
"ANDROID_BT",
|
|
227
|
+
"ANDROID_AUTO"
|
|
228
|
+
];
|
|
229
|
+
/** Ficha §6: filled per kiosk in the device settings. */
|
|
230
|
+
const clisitefPinPadDeviceFields = [
|
|
231
|
+
{
|
|
232
|
+
key: "sitef_ip",
|
|
233
|
+
label: "SiTef server IP",
|
|
234
|
+
type: "text",
|
|
235
|
+
required: true
|
|
236
|
+
},
|
|
237
|
+
{
|
|
238
|
+
key: "store_code",
|
|
239
|
+
label: "SiTef store code",
|
|
240
|
+
type: "text",
|
|
241
|
+
required: true
|
|
242
|
+
},
|
|
243
|
+
{
|
|
244
|
+
key: "terminal_number",
|
|
245
|
+
label: "SiTef terminal number",
|
|
246
|
+
type: "text",
|
|
247
|
+
required: true
|
|
248
|
+
},
|
|
249
|
+
{
|
|
250
|
+
key: "cnpj_store",
|
|
251
|
+
label: "Store CNPJ",
|
|
252
|
+
type: "text",
|
|
253
|
+
required: true
|
|
254
|
+
},
|
|
255
|
+
{
|
|
256
|
+
key: "pinpad_type",
|
|
257
|
+
label: "Pinpad connection",
|
|
258
|
+
type: "select",
|
|
259
|
+
required: true,
|
|
260
|
+
options: [...CLISITEF_PINPAD_TYPES]
|
|
261
|
+
}
|
|
262
|
+
];
|
|
263
|
+
const text = (value) => {
|
|
264
|
+
return typeof value === "string" ? value.trim() : "";
|
|
265
|
+
};
|
|
266
|
+
const notConfigured = (missing) => {
|
|
267
|
+
return new PaymentError(createPaymentError("TERMINAL_NOT_CONFIGURED", { message: `CliSiTef is missing ${missing.join(", ")}` }));
|
|
268
|
+
};
|
|
269
|
+
const isModality = (value) => {
|
|
270
|
+
return value === "DEBIT" || value === "CREDIT" || value === "VOUCHER";
|
|
271
|
+
};
|
|
272
|
+
/** `payments.config` (merchant) + device settings; throws when incomplete. */
|
|
273
|
+
const readClisitefPinPadSettings = (config, device) => {
|
|
274
|
+
const modality = text(config.modality);
|
|
275
|
+
const cnpjEnterprise = text(config.cnpj_enterprise);
|
|
276
|
+
const serverIp = text(device.sitef_ip);
|
|
277
|
+
const storeCode = text(device.store_code);
|
|
278
|
+
const terminalNumber = text(device.terminal_number);
|
|
279
|
+
const cnpjStore = text(device.cnpj_store).replace(/\D/g, "");
|
|
280
|
+
const pinpadType = text(device.pinpad_type) || void 0;
|
|
281
|
+
const required = {
|
|
282
|
+
modality,
|
|
283
|
+
cnpj_enterprise: cnpjEnterprise
|
|
284
|
+
};
|
|
285
|
+
const fromDevice = {
|
|
286
|
+
sitef_ip: serverIp,
|
|
287
|
+
store_code: storeCode
|
|
288
|
+
};
|
|
289
|
+
const more = {
|
|
290
|
+
terminal_number: terminalNumber,
|
|
291
|
+
cnpj_store: cnpjStore
|
|
292
|
+
};
|
|
293
|
+
const values = {
|
|
294
|
+
...required,
|
|
295
|
+
...fromDevice
|
|
296
|
+
};
|
|
297
|
+
Object.assign(values, more);
|
|
298
|
+
const missing = Object.keys(values).filter((key) => !values[key]);
|
|
299
|
+
if (missing.length) throw notConfigured(missing);
|
|
300
|
+
if (!isModality(modality)) throw notConfigured(["a valid modality"]);
|
|
301
|
+
return {
|
|
302
|
+
configure: {
|
|
303
|
+
serverIp,
|
|
304
|
+
storeCode,
|
|
305
|
+
terminalNumber,
|
|
306
|
+
additionalParams: clisitefAdditionalParams({
|
|
307
|
+
cnpjStore,
|
|
308
|
+
cnpjEnterprise,
|
|
309
|
+
pinpadType
|
|
310
|
+
})
|
|
311
|
+
},
|
|
312
|
+
modality,
|
|
313
|
+
operator: text(config.operator) || "KIOSKO_BR",
|
|
314
|
+
restrictions: text(config.restrictions),
|
|
315
|
+
terminalNumber
|
|
316
|
+
};
|
|
317
|
+
};
|
|
318
|
+
//#endregion
|
|
319
|
+
//#region src/store.ts
|
|
320
|
+
const isRecord = (value) => {
|
|
321
|
+
if (!value || typeof value !== "object") return false;
|
|
322
|
+
const record = value;
|
|
323
|
+
return typeof record.fiscalCoupon === "string" && typeof record.attemptReference === "string";
|
|
324
|
+
};
|
|
325
|
+
const readTransaction = async (storage) => {
|
|
326
|
+
const raw = await storage.get(CLISITEF_STORAGE_KEY);
|
|
327
|
+
if (!raw) return null;
|
|
328
|
+
try {
|
|
329
|
+
const parsed = JSON.parse(raw);
|
|
330
|
+
return isRecord(parsed) ? parsed : null;
|
|
331
|
+
} catch {
|
|
332
|
+
return null;
|
|
333
|
+
}
|
|
334
|
+
};
|
|
335
|
+
const writeTransaction = async (storage, record) => {
|
|
336
|
+
await storage.set(CLISITEF_STORAGE_KEY, JSON.stringify(record));
|
|
337
|
+
};
|
|
338
|
+
const isOpen = (record) => {
|
|
339
|
+
return record.state === "started" || record.state === "approved";
|
|
340
|
+
};
|
|
341
|
+
//#endregion
|
|
342
|
+
//#region src/transaction.ts
|
|
343
|
+
const errorCode = (error) => {
|
|
344
|
+
if (!error || typeof error !== "object") return null;
|
|
345
|
+
const code = error.code;
|
|
346
|
+
return typeof code === "string" || typeof code === "number" ? code : null;
|
|
347
|
+
};
|
|
348
|
+
const errorMessage = (error) => {
|
|
349
|
+
return error instanceof Error ? error.message : null;
|
|
350
|
+
};
|
|
351
|
+
const inputType = (command) => {
|
|
352
|
+
if (command === CLISITEF_COMMAND.READ_MASKED) return "password";
|
|
353
|
+
if (command === CLISITEF_COMMAND.READ_AMOUNT) return "number";
|
|
354
|
+
return "text";
|
|
355
|
+
};
|
|
356
|
+
const MESSAGES = /* @__PURE__ */ new Set([
|
|
357
|
+
CLISITEF_COMMAND.MESSAGE_OPERATOR,
|
|
358
|
+
CLISITEF_COMMAND.MESSAGE_CUSTOMER,
|
|
359
|
+
CLISITEF_COMMAND.MESSAGE_BOTH,
|
|
360
|
+
CLISITEF_COMMAND.PRESS_KEY,
|
|
361
|
+
CLISITEF_COMMAND.FOOTER
|
|
362
|
+
]);
|
|
363
|
+
const SILENT = /* @__PURE__ */ new Set([
|
|
364
|
+
CLISITEF_COMMAND.CLEAR_OPERATOR,
|
|
365
|
+
CLISITEF_COMMAND.CLEAR_CUSTOMER,
|
|
366
|
+
CLISITEF_COMMAND.CLEAR_BOTH,
|
|
367
|
+
CLISITEF_COMMAND.HEADER,
|
|
368
|
+
CLISITEF_COMMAND.CLEAR_HEADER,
|
|
369
|
+
CLISITEF_COMMAND.CHECK_ABORT
|
|
370
|
+
]);
|
|
371
|
+
const READS = /* @__PURE__ */ new Set([
|
|
372
|
+
CLISITEF_COMMAND.READ_FIELD,
|
|
373
|
+
CLISITEF_COMMAND.READ_AMOUNT,
|
|
374
|
+
CLISITEF_COMMAND.READ_MASKED
|
|
375
|
+
]);
|
|
376
|
+
/**
|
|
377
|
+
* One CliSiTef transaction: `startTransaction`, the `onData` loop answered
|
|
378
|
+
* through `ui`, `finishTransaction(1)` on approval (`stage 1` = 0) and
|
|
379
|
+
* success only when `stage 2` closes with 0. Never rejects.
|
|
380
|
+
*/
|
|
381
|
+
const runClisitefTransaction = (input) => {
|
|
382
|
+
const { client, ui, signal } = input;
|
|
383
|
+
const fields = /* @__PURE__ */ new Map();
|
|
384
|
+
const subscriptions = [];
|
|
385
|
+
let title = "";
|
|
386
|
+
let captured = false;
|
|
387
|
+
let approved = false;
|
|
388
|
+
let closing = false;
|
|
389
|
+
let aborted = false;
|
|
390
|
+
let watchdog;
|
|
391
|
+
let queue = Promise.resolve();
|
|
392
|
+
let resolve = () => void 0;
|
|
393
|
+
const done = new Promise((r) => resolve = r);
|
|
394
|
+
const settle = (status, reason) => {
|
|
395
|
+
clearTimeout(watchdog);
|
|
396
|
+
for (const subscription of subscriptions) subscription.remove();
|
|
397
|
+
signal.removeEventListener("abort", onAbort);
|
|
398
|
+
resolve({
|
|
399
|
+
status,
|
|
400
|
+
...reason,
|
|
401
|
+
fields,
|
|
402
|
+
aborted
|
|
403
|
+
});
|
|
404
|
+
};
|
|
405
|
+
const undo = async (reason) => {
|
|
406
|
+
try {
|
|
407
|
+
const p1 = {
|
|
408
|
+
confirm: CLISITEF_CONFIRM.UNDO,
|
|
409
|
+
...input.finish
|
|
410
|
+
};
|
|
411
|
+
await client.finishTransaction(p1);
|
|
412
|
+
settle("undone", reason);
|
|
413
|
+
} catch (error) {
|
|
414
|
+
settle("unresolved", {
|
|
415
|
+
...reason,
|
|
416
|
+
message: errorMessage(error)
|
|
417
|
+
});
|
|
418
|
+
}
|
|
419
|
+
};
|
|
420
|
+
const close = async (reason) => {
|
|
421
|
+
if (closing) return;
|
|
422
|
+
closing = true;
|
|
423
|
+
if (!approved) await client.abortTransaction(CLISITEF_ABORT_FLAG).catch(() => 0);
|
|
424
|
+
if (captured || approved) return undo(reason);
|
|
425
|
+
settle("declined", reason);
|
|
426
|
+
};
|
|
427
|
+
function onAbort() {
|
|
428
|
+
aborted = true;
|
|
429
|
+
close({
|
|
430
|
+
code: null,
|
|
431
|
+
message: "Aborted by the payment deadline"
|
|
432
|
+
});
|
|
433
|
+
}
|
|
434
|
+
const reply = async (data) => {
|
|
435
|
+
if (closing) return;
|
|
436
|
+
await client.continueTransaction(data);
|
|
437
|
+
};
|
|
438
|
+
const onData = async (event) => {
|
|
439
|
+
const { command, input: text } = event;
|
|
440
|
+
if (command === CLISITEF_COMMAND.STORE_FIELD) {
|
|
441
|
+
fields.set(event.fieldId, text);
|
|
442
|
+
if (CLISITEF_CAPTURE_FIELDS.has(event.fieldId)) captured = true;
|
|
443
|
+
return reply("");
|
|
444
|
+
}
|
|
445
|
+
if (MESSAGES.has(command)) {
|
|
446
|
+
if (text.trim()) ui.message({
|
|
447
|
+
text: text.trim(),
|
|
448
|
+
kind: "waiting"
|
|
449
|
+
});
|
|
450
|
+
return reply("");
|
|
451
|
+
}
|
|
452
|
+
if (command === CLISITEF_COMMAND.MENU_TITLE) {
|
|
453
|
+
title = text.trim();
|
|
454
|
+
return reply("");
|
|
455
|
+
}
|
|
456
|
+
if (command === CLISITEF_COMMAND.CLEAR_MENU_TITLE) {
|
|
457
|
+
title = "";
|
|
458
|
+
return reply("");
|
|
459
|
+
}
|
|
460
|
+
if (SILENT.has(command)) return reply("");
|
|
461
|
+
if (command === CLISITEF_COMMAND.CONFIRM) {
|
|
462
|
+
const yes = await ui.confirm({ text: text.trim() || title });
|
|
463
|
+
return reply(yes ? "0" : "1");
|
|
464
|
+
}
|
|
465
|
+
if (command === CLISITEF_COMMAND.MENU) {
|
|
466
|
+
const options = parseClisitefMenu(text);
|
|
467
|
+
const label = title || "Selecione uma opção";
|
|
468
|
+
return reply(await ui.select({
|
|
469
|
+
title: label,
|
|
470
|
+
options
|
|
471
|
+
}));
|
|
472
|
+
}
|
|
473
|
+
if (READS.has(command)) {
|
|
474
|
+
const label = text.trim() || title || "Informe o dado";
|
|
475
|
+
const max = event.maxLength > 0 ? event.maxLength : void 0;
|
|
476
|
+
const p1 = {
|
|
477
|
+
label,
|
|
478
|
+
type: inputType(command),
|
|
479
|
+
maxLength: max
|
|
480
|
+
};
|
|
481
|
+
return reply(await ui.input(p1));
|
|
482
|
+
}
|
|
483
|
+
const message = `Unsupported CliSiTef command ${command}`;
|
|
484
|
+
return close({
|
|
485
|
+
code: `UNSUPPORTED_COMMAND_${command}`,
|
|
486
|
+
message
|
|
487
|
+
});
|
|
488
|
+
};
|
|
489
|
+
const confirm = () => {
|
|
490
|
+
const message = "CliSiTef did not close the confirmation";
|
|
491
|
+
const expire = () => {
|
|
492
|
+
if (closing) return;
|
|
493
|
+
closing = true;
|
|
494
|
+
undo({
|
|
495
|
+
code: null,
|
|
496
|
+
message
|
|
497
|
+
});
|
|
498
|
+
};
|
|
499
|
+
watchdog = setTimeout(expire, input.confirmationTimeoutMs);
|
|
500
|
+
const p1 = {
|
|
501
|
+
confirm: CLISITEF_CONFIRM.CONFIRM,
|
|
502
|
+
...input.finish
|
|
503
|
+
};
|
|
504
|
+
client.finishTransaction(p1).catch((error) => {
|
|
505
|
+
if (closing) return;
|
|
506
|
+
closing = true;
|
|
507
|
+
clearTimeout(watchdog);
|
|
508
|
+
undo({
|
|
509
|
+
code: errorCode(error),
|
|
510
|
+
message: errorMessage(error)
|
|
511
|
+
});
|
|
512
|
+
});
|
|
513
|
+
};
|
|
514
|
+
const onResult = async (event) => {
|
|
515
|
+
const { stage, resultCode: code } = event;
|
|
516
|
+
if (closing) return;
|
|
517
|
+
if (stage === 1 && code === CLISITEF_RESULT.OK) {
|
|
518
|
+
approved = true;
|
|
519
|
+
await input.onApproved(fields);
|
|
520
|
+
if (!closing) confirm();
|
|
521
|
+
return;
|
|
522
|
+
}
|
|
523
|
+
if (stage === 1) {
|
|
524
|
+
closing = true;
|
|
525
|
+
if (!(captured && CLISITEF_INCONCLUSIVE_CODES.has(code))) return settle("declined", {
|
|
526
|
+
code,
|
|
527
|
+
message: null
|
|
528
|
+
});
|
|
529
|
+
await client.abortTransaction(CLISITEF_ABORT_FLAG).catch(() => 0);
|
|
530
|
+
return undo({
|
|
531
|
+
code,
|
|
532
|
+
message: null
|
|
533
|
+
});
|
|
534
|
+
}
|
|
535
|
+
if (!approved) return;
|
|
536
|
+
closing = true;
|
|
537
|
+
if (code === CLISITEF_RESULT.OK) return settle("confirmed", {
|
|
538
|
+
code,
|
|
539
|
+
message: null
|
|
540
|
+
});
|
|
541
|
+
return undo({
|
|
542
|
+
code,
|
|
543
|
+
message: null
|
|
544
|
+
});
|
|
545
|
+
};
|
|
546
|
+
const enqueue = (handle) => {
|
|
547
|
+
const run = async () => {
|
|
548
|
+
await handle();
|
|
549
|
+
};
|
|
550
|
+
queue = queue.then(run).catch(async (error) => {
|
|
551
|
+
const reason = {
|
|
552
|
+
code: errorCode(error),
|
|
553
|
+
message: errorMessage(error)
|
|
554
|
+
};
|
|
555
|
+
if (closing) return;
|
|
556
|
+
await close({
|
|
557
|
+
...reason,
|
|
558
|
+
error
|
|
559
|
+
});
|
|
560
|
+
});
|
|
561
|
+
};
|
|
562
|
+
const begin = async () => {
|
|
563
|
+
if (signal.aborted) {
|
|
564
|
+
aborted = true;
|
|
565
|
+
return settle("declined", {
|
|
566
|
+
code: null,
|
|
567
|
+
message: "Aborted"
|
|
568
|
+
});
|
|
569
|
+
}
|
|
570
|
+
signal.addEventListener("abort", onAbort);
|
|
571
|
+
const handleData = (e) => enqueue(() => onData(e));
|
|
572
|
+
const handleResult = (e) => {
|
|
573
|
+
enqueue(() => onResult(e));
|
|
574
|
+
};
|
|
575
|
+
subscriptions.push(client.onData(handleData));
|
|
576
|
+
subscriptions.push(client.onTransactionResult(handleResult));
|
|
577
|
+
try {
|
|
578
|
+
const code = await client.startTransaction(input.start);
|
|
579
|
+
if (code === CLISITEF_RESULT.CONTINUE || closing) return;
|
|
580
|
+
closing = true;
|
|
581
|
+
settle("declined", {
|
|
582
|
+
code,
|
|
583
|
+
message: null
|
|
584
|
+
});
|
|
585
|
+
} catch (error) {
|
|
586
|
+
if (closing) return;
|
|
587
|
+
closing = true;
|
|
588
|
+
const reason = {
|
|
589
|
+
code: errorCode(error),
|
|
590
|
+
message: errorMessage(error)
|
|
591
|
+
};
|
|
592
|
+
settle("declined", reason);
|
|
593
|
+
}
|
|
594
|
+
};
|
|
595
|
+
begin();
|
|
596
|
+
return done;
|
|
597
|
+
};
|
|
598
|
+
//#endregion
|
|
599
|
+
//#region src/adapter.ts
|
|
600
|
+
/** Confirmed in the act (`finishTransaction(1)`), never captured later. */
|
|
601
|
+
const clisitefPinPadCapabilities = {
|
|
602
|
+
deferredCapture: false,
|
|
603
|
+
inPerson: true
|
|
604
|
+
};
|
|
605
|
+
const createLock = () => {
|
|
606
|
+
let tail = Promise.resolve();
|
|
607
|
+
return (task) => {
|
|
608
|
+
const run = tail.then(task, task);
|
|
609
|
+
tail = run.catch(() => void 0);
|
|
610
|
+
return run;
|
|
611
|
+
};
|
|
612
|
+
};
|
|
613
|
+
const failed = (record, reference, error, needsReview = false) => {
|
|
614
|
+
const base = record.metadata ?? { attempt_reference: reference };
|
|
615
|
+
const review = needsReview ? { needs_review: true } : {};
|
|
616
|
+
const metadata = {
|
|
617
|
+
...base,
|
|
618
|
+
attempt_reference: reference,
|
|
619
|
+
error,
|
|
620
|
+
...review
|
|
621
|
+
};
|
|
622
|
+
return {
|
|
623
|
+
status: "FAILED",
|
|
624
|
+
amount: record.amount,
|
|
625
|
+
metadata,
|
|
626
|
+
error
|
|
627
|
+
};
|
|
628
|
+
};
|
|
629
|
+
const paid = (record) => {
|
|
630
|
+
const reference = record.attemptReference;
|
|
631
|
+
const metadata = record.metadata ?? { attempt_reference: reference };
|
|
632
|
+
const details = record.details ?? null;
|
|
633
|
+
return {
|
|
634
|
+
status: "PAID",
|
|
635
|
+
amount: record.amount,
|
|
636
|
+
metadata,
|
|
637
|
+
details
|
|
638
|
+
};
|
|
639
|
+
};
|
|
640
|
+
const outcomeError = (outcome, attempt) => {
|
|
641
|
+
const provider = {
|
|
642
|
+
provider_message: outcome.message,
|
|
643
|
+
attempt
|
|
644
|
+
};
|
|
645
|
+
const code = outcome.code == null ? null : String(outcome.code);
|
|
646
|
+
const extra = {
|
|
647
|
+
...provider,
|
|
648
|
+
provider_code: code
|
|
649
|
+
};
|
|
650
|
+
if (outcome.aborted) return createPaymentError("TIMEOUT", extra);
|
|
651
|
+
if (outcome.status === "unresolved") return createPaymentError("AMBIGUOUS", extra);
|
|
652
|
+
if (outcome.code === null) {
|
|
653
|
+
const message = outcome.message ?? void 0;
|
|
654
|
+
return createPaymentError("PROVIDER_UNAVAILABLE", {
|
|
655
|
+
...extra,
|
|
656
|
+
retryable: false,
|
|
657
|
+
message
|
|
658
|
+
});
|
|
659
|
+
}
|
|
660
|
+
const fallback = typeof outcome.code === "number" && outcome.code > 0 ? "CARD_DECLINED" : "UNKNOWN";
|
|
661
|
+
return toPaymentError(clisitefPinPadErrors, {
|
|
662
|
+
code: outcome.code,
|
|
663
|
+
message: outcome.message,
|
|
664
|
+
attempt,
|
|
665
|
+
fallback
|
|
666
|
+
});
|
|
667
|
+
};
|
|
668
|
+
const terminalError = (error) => {
|
|
669
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
670
|
+
return {
|
|
671
|
+
ok: false,
|
|
672
|
+
message,
|
|
673
|
+
error: createPaymentError("TERMINAL_UNAVAILABLE", { message })
|
|
674
|
+
};
|
|
675
|
+
};
|
|
676
|
+
/**
|
|
677
|
+
* CliSiTef (Software Express) on the kiosk pinpad, Brazil. One adapter per
|
|
678
|
+
* modality payment (DEBIT / CREDIT / VOUCHER in `payments.config`). The
|
|
679
|
+
* pinpad waits on the customer: give the payment a `runtime.timeout_ms`
|
|
680
|
+
* well above the 30 s default (e.g. 90 000).
|
|
681
|
+
*/
|
|
682
|
+
const clisitefPinPadPayments = (options = {}) => {
|
|
683
|
+
const client = options.client ?? clisitef;
|
|
684
|
+
const now = options.now ?? (() => /* @__PURE__ */ new Date());
|
|
685
|
+
const confirmationMs = options.confirmationTimeoutMs ?? 6e4;
|
|
686
|
+
const lock = createLock();
|
|
687
|
+
const storageOf = (ctx) => {
|
|
688
|
+
return options.storage ?? ctx.storage;
|
|
689
|
+
};
|
|
690
|
+
const configure = async (settings) => {
|
|
691
|
+
const code = await client.configure(settings.configure);
|
|
692
|
+
if (code === 0) return null;
|
|
693
|
+
client.invalidate();
|
|
694
|
+
return toPaymentError(clisitefPinPadErrors, {
|
|
695
|
+
code,
|
|
696
|
+
fallback: "TERMINAL_NOT_CONFIGURED"
|
|
697
|
+
});
|
|
698
|
+
};
|
|
699
|
+
/** Undoes a transaction a crash or a lost result left open. */
|
|
700
|
+
const recover = async (storage, record) => {
|
|
701
|
+
const { fiscalCoupon, fiscalDate, time } = record;
|
|
702
|
+
const count = client.getQttPendingTransactions;
|
|
703
|
+
const pending = await count(fiscalDate, fiscalCoupon).catch(() => null);
|
|
704
|
+
if (pending === null) return record;
|
|
705
|
+
let next;
|
|
706
|
+
if (pending > 0) {
|
|
707
|
+
const p1 = {
|
|
708
|
+
confirm: CLISITEF_CONFIRM.UNDO,
|
|
709
|
+
fiscalCoupon,
|
|
710
|
+
fiscalDate
|
|
711
|
+
};
|
|
712
|
+
if (await client.finishTransaction({
|
|
713
|
+
...p1,
|
|
714
|
+
time
|
|
715
|
+
}).catch(() => -1) !== 0) return record;
|
|
716
|
+
const error = createPaymentError("PROVIDER_UNAVAILABLE", {
|
|
717
|
+
retryable: false,
|
|
718
|
+
message: "Pending CliSiTef transaction undone"
|
|
719
|
+
});
|
|
720
|
+
next = {
|
|
721
|
+
...record,
|
|
722
|
+
state: "undone",
|
|
723
|
+
error
|
|
724
|
+
};
|
|
725
|
+
} else if (record.state === "approved") {
|
|
726
|
+
const error = createPaymentError("AMBIGUOUS");
|
|
727
|
+
next = {
|
|
728
|
+
...record,
|
|
729
|
+
state: "review",
|
|
730
|
+
error
|
|
731
|
+
};
|
|
732
|
+
} else {
|
|
733
|
+
const error = createPaymentError("PROVIDER_UNAVAILABLE", {
|
|
734
|
+
retryable: false,
|
|
735
|
+
message: "CliSiTef transaction never approved"
|
|
736
|
+
});
|
|
737
|
+
next = {
|
|
738
|
+
...record,
|
|
739
|
+
state: "undone",
|
|
740
|
+
error
|
|
741
|
+
};
|
|
742
|
+
}
|
|
743
|
+
await writeTransaction(storage, next);
|
|
744
|
+
return next;
|
|
745
|
+
};
|
|
746
|
+
/** Configures and resolves the open record of the device, if any. */
|
|
747
|
+
const prepare = async (ctx) => {
|
|
748
|
+
const settings = readClisitefPinPadSettings(ctx.config, ctx.deviceSettings);
|
|
749
|
+
const error = await configure(settings);
|
|
750
|
+
const storage = storageOf(ctx);
|
|
751
|
+
let record = await readTransaction(storage);
|
|
752
|
+
if (!error && record && isOpen(record)) record = await recover(storage, record);
|
|
753
|
+
return {
|
|
754
|
+
settings,
|
|
755
|
+
error,
|
|
756
|
+
storage,
|
|
757
|
+
record
|
|
758
|
+
};
|
|
759
|
+
};
|
|
760
|
+
const fromRecord = (record, ctx) => {
|
|
761
|
+
const reference = ctx.attemptReference;
|
|
762
|
+
if (record.state === "confirmed") return paid(record);
|
|
763
|
+
if (record.state === "review") {
|
|
764
|
+
const error = record.error ?? createPaymentError("AMBIGUOUS");
|
|
765
|
+
return failed(record, reference, error, true);
|
|
766
|
+
}
|
|
767
|
+
if (record.state === "undone") {
|
|
768
|
+
const fallback = createPaymentError("UNKNOWN", { retryable: false });
|
|
769
|
+
return failed(record, reference, record.error ?? fallback);
|
|
770
|
+
}
|
|
771
|
+
const error = createPaymentError("TIMEOUT", { attempt: ctx.attempt });
|
|
772
|
+
return failed(record, reference, error);
|
|
773
|
+
};
|
|
774
|
+
const charge = async (ctx) => {
|
|
775
|
+
const amount = ctx.amount.value;
|
|
776
|
+
const reference = ctx.attemptReference;
|
|
777
|
+
const empty = { amount };
|
|
778
|
+
if (!client.available) {
|
|
779
|
+
const error = createPaymentError("TERMINAL_UNAVAILABLE");
|
|
780
|
+
return failed(empty, reference, error);
|
|
781
|
+
}
|
|
782
|
+
const prepared = await prepare(ctx);
|
|
783
|
+
const { settings, storage, record: previous } = prepared;
|
|
784
|
+
if (prepared.error) return failed(empty, reference, prepared.error);
|
|
785
|
+
if (previous?.attemptReference === reference && previous?.state === "confirmed") return paid(previous);
|
|
786
|
+
if (previous && isOpen(previous)) {
|
|
787
|
+
const error = createPaymentError("TERMINAL_BUSY", { attempt: ctx.attempt });
|
|
788
|
+
return failed(empty, reference, error);
|
|
789
|
+
}
|
|
790
|
+
const date = now();
|
|
791
|
+
const fiscal = clisitefFiscalDateTime(date, options.timeZone);
|
|
792
|
+
const fiscalCoupon = String(date.getTime());
|
|
793
|
+
const finish = {
|
|
794
|
+
fiscalCoupon,
|
|
795
|
+
...fiscal
|
|
796
|
+
};
|
|
797
|
+
const record = {
|
|
798
|
+
attemptReference: reference,
|
|
799
|
+
...finish,
|
|
800
|
+
amount,
|
|
801
|
+
startedAt: date.getTime(),
|
|
802
|
+
state: "started"
|
|
803
|
+
};
|
|
804
|
+
await writeTransaction(storage, record);
|
|
805
|
+
const s1 = {
|
|
806
|
+
modality: CLISITEF_MODALITY[settings.modality],
|
|
807
|
+
value: formatClisitefAmount(amount),
|
|
808
|
+
...finish
|
|
809
|
+
};
|
|
810
|
+
const s2 = { operator: settings.operator };
|
|
811
|
+
const start = {
|
|
812
|
+
...s1,
|
|
813
|
+
...s2,
|
|
814
|
+
restrictions: settings.restrictions
|
|
815
|
+
};
|
|
816
|
+
const meta = {
|
|
817
|
+
modality: settings.modality,
|
|
818
|
+
fiscalCoupon
|
|
819
|
+
};
|
|
820
|
+
const m1 = {
|
|
821
|
+
terminalNumber: settings.terminalNumber,
|
|
822
|
+
...meta
|
|
823
|
+
};
|
|
824
|
+
const metadataOf = (fields) => {
|
|
825
|
+
const m2 = {
|
|
826
|
+
attemptReference: reference,
|
|
827
|
+
fields
|
|
828
|
+
};
|
|
829
|
+
return clisitefMetadata({
|
|
830
|
+
...m1,
|
|
831
|
+
...m2,
|
|
832
|
+
fiscalDate: fiscal.fiscalDate
|
|
833
|
+
});
|
|
834
|
+
};
|
|
835
|
+
const onApproved = async (fields) => {
|
|
836
|
+
const metadata = metadataOf(fields);
|
|
837
|
+
await writeTransaction(storage, {
|
|
838
|
+
...record,
|
|
839
|
+
state: "approved",
|
|
840
|
+
metadata
|
|
841
|
+
});
|
|
842
|
+
};
|
|
843
|
+
const t1 = {
|
|
844
|
+
client,
|
|
845
|
+
ui: ctx.ui,
|
|
846
|
+
signal: ctx.signal,
|
|
847
|
+
start,
|
|
848
|
+
finish
|
|
849
|
+
};
|
|
850
|
+
const t2 = {
|
|
851
|
+
confirmationTimeoutMs: confirmationMs,
|
|
852
|
+
onApproved
|
|
853
|
+
};
|
|
854
|
+
const outcome = await runClisitefTransaction({
|
|
855
|
+
...t1,
|
|
856
|
+
...t2
|
|
857
|
+
});
|
|
858
|
+
const metadata = metadataOf(outcome.fields);
|
|
859
|
+
if (outcome.status === "confirmed") {
|
|
860
|
+
const details = clisitefDetails(metadata);
|
|
861
|
+
const confirmed = {
|
|
862
|
+
...record,
|
|
863
|
+
metadata,
|
|
864
|
+
details,
|
|
865
|
+
state: "confirmed"
|
|
866
|
+
};
|
|
867
|
+
await writeTransaction(storage, confirmed);
|
|
868
|
+
return paid(confirmed);
|
|
869
|
+
}
|
|
870
|
+
if (outcome.code === -1) client.invalidate();
|
|
871
|
+
const error = outcomeError(outcome, ctx.attempt);
|
|
872
|
+
const unresolved = outcome.status === "unresolved";
|
|
873
|
+
const state = unresolved ? "approved" : "undone";
|
|
874
|
+
const closed = {
|
|
875
|
+
...record,
|
|
876
|
+
state,
|
|
877
|
+
metadata,
|
|
878
|
+
error
|
|
879
|
+
};
|
|
880
|
+
await writeTransaction(storage, closed);
|
|
881
|
+
if (outcome.error) throw outcome.error;
|
|
882
|
+
return failed(closed, reference, error, unresolved);
|
|
883
|
+
};
|
|
884
|
+
const query = async (ctx, result) => {
|
|
885
|
+
const reference = result.metadata.attempt_reference;
|
|
886
|
+
const scoped = {
|
|
887
|
+
...ctx,
|
|
888
|
+
attemptReference: reference
|
|
889
|
+
};
|
|
890
|
+
const { record, error } = await prepare(scoped);
|
|
891
|
+
if (record?.attemptReference !== reference) {
|
|
892
|
+
const missing = createPaymentError("UNKNOWN", {
|
|
893
|
+
retryable: false,
|
|
894
|
+
message: "No CliSiTef transaction for this attempt"
|
|
895
|
+
});
|
|
896
|
+
return failed(result, reference, missing);
|
|
897
|
+
}
|
|
898
|
+
if (error && isOpen(record)) return failed(record, reference, error);
|
|
899
|
+
return fromRecord(record, scoped);
|
|
900
|
+
};
|
|
901
|
+
const cancel = async (ctx, result) => {
|
|
902
|
+
const next = await query(ctx, result);
|
|
903
|
+
if (next.status === "PAID") {
|
|
904
|
+
const error = createPaymentError("REVERSAL_NOT_ALLOWED");
|
|
905
|
+
return {
|
|
906
|
+
...next,
|
|
907
|
+
status: "FAILED",
|
|
908
|
+
error
|
|
909
|
+
};
|
|
910
|
+
}
|
|
911
|
+
const code = next.error?.code;
|
|
912
|
+
if (!(code !== "AMBIGUOUS" && code !== "TIMEOUT")) return next;
|
|
913
|
+
return {
|
|
914
|
+
...next,
|
|
915
|
+
status: "CANCELLED",
|
|
916
|
+
error: void 0
|
|
917
|
+
};
|
|
918
|
+
};
|
|
919
|
+
const operation = (run) => {
|
|
920
|
+
return (ctx) => {
|
|
921
|
+
const task = async () => {
|
|
922
|
+
try {
|
|
923
|
+
const data = await run(ctx);
|
|
924
|
+
return {
|
|
925
|
+
ok: true,
|
|
926
|
+
message: typeof data === "string" ? data : null,
|
|
927
|
+
data: data && typeof data === "object" ? data : void 0
|
|
928
|
+
};
|
|
929
|
+
} catch (error) {
|
|
930
|
+
return terminalError(error);
|
|
931
|
+
}
|
|
932
|
+
};
|
|
933
|
+
return lock(task);
|
|
934
|
+
};
|
|
935
|
+
};
|
|
936
|
+
const terminal = {
|
|
937
|
+
init: operation(async (ctx) => {
|
|
938
|
+
client.invalidate();
|
|
939
|
+
const s1 = readClisitefPinPadSettings(ctx.config, ctx.deviceSettings);
|
|
940
|
+
const error = await configure(s1);
|
|
941
|
+
if (error) throw new Error(error.message);
|
|
942
|
+
return "CliSiTef configured";
|
|
943
|
+
}),
|
|
944
|
+
test: operation(async () => `CliSiTef ${await client.getVersion()}`),
|
|
945
|
+
status: operation(async () => client.getHealth() ?? {}),
|
|
946
|
+
extra: [{
|
|
947
|
+
key: "submit_pending_messages",
|
|
948
|
+
label: "Send pending messages",
|
|
949
|
+
run: operation(async () => {
|
|
950
|
+
return `Pending messages sent (${await client.submitPendingMessages()})`;
|
|
951
|
+
})
|
|
952
|
+
}, {
|
|
953
|
+
key: "release_pinpad",
|
|
954
|
+
label: "Release pinpad",
|
|
955
|
+
run: operation(async () => {
|
|
956
|
+
await client.closePinPad();
|
|
957
|
+
await client.disconnectSitef();
|
|
958
|
+
client.invalidate();
|
|
959
|
+
return "Pinpad released";
|
|
960
|
+
})
|
|
961
|
+
}]
|
|
962
|
+
};
|
|
963
|
+
return {
|
|
964
|
+
provider: "clisitef",
|
|
965
|
+
method: "PIN_PAD",
|
|
966
|
+
deviceFields: clisitefPinPadDeviceFields,
|
|
967
|
+
errors: clisitefPinPadErrors,
|
|
968
|
+
capabilities: clisitefPinPadCapabilities,
|
|
969
|
+
start: (ctx) => lock(() => charge(ctx)),
|
|
970
|
+
query: (ctx, result) => lock(() => query(ctx, result)),
|
|
971
|
+
cancel: (ctx, result) => lock(() => cancel(ctx, result)),
|
|
972
|
+
reversalFor: (result) => result.status === "PAID" ? "manual" : "cancel",
|
|
973
|
+
terminal
|
|
974
|
+
};
|
|
975
|
+
};
|
|
976
|
+
//#endregion
|
|
977
|
+
export { CLISITEF_CANCEL_CODES, CLISITEF_CAPTURE_FIELDS, CLISITEF_COMMAND, CLISITEF_CONFIRMATION_TIMEOUT_MS, CLISITEF_DEFAULT_OPERATOR, CLISITEF_FIELD, CLISITEF_INCONCLUSIVE_CODES, CLISITEF_NO, CLISITEF_PINPAD_TYPES, CLISITEF_STORAGE_KEY, CLISITEF_YES, clisitefDetails, clisitefLast4, clisitefMetadata, clisitefPinPadCapabilities, clisitefPinPadDeviceFields, clisitefPinPadErrors, clisitefPinPadPayments, isOpen, readClisitefPinPadSettings, readTransaction, runClisitefTransaction, writeTransaction };
|
package/package.json
CHANGED
|
@@ -1,6 +1,61 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fire-spark/payments-clisitef-pinpad-rn",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
3
|
+
"version": "0.7.0",
|
|
4
|
+
"description": "Fire Spark CliSiTef (Software Express) pinpad adapter for Brazil: debit, credit and voucher on Android kiosks",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"sideEffects": false,
|
|
7
|
+
"files": [
|
|
8
|
+
"dist"
|
|
9
|
+
],
|
|
10
|
+
"main": "./dist/index.js",
|
|
11
|
+
"types": "./dist/index.d.ts",
|
|
12
|
+
"exports": {
|
|
13
|
+
".": {
|
|
14
|
+
"types": "./dist/index.d.ts",
|
|
15
|
+
"import": "./dist/index.js",
|
|
16
|
+
"default": "./dist/index.js"
|
|
17
|
+
}
|
|
18
|
+
},
|
|
19
|
+
"dependencies": {
|
|
20
|
+
"@fire-spark/payments": "^0.7.0",
|
|
21
|
+
"@fire-spark/payments-clisitef-client": "^0.7.0",
|
|
22
|
+
"@fire-spark/types": "^0.7.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/payments",
|
|
47
|
+
"@fire-spark/payments-clisitef-client",
|
|
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