@hw-connector/client 0.0.0-stage → 2.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/README.md +41 -3
- package/dist/index.d.ts +239 -0
- package/dist/index.js +299 -0
- package/dist/types.d.ts +270 -0
- package/dist/types.js +2 -0
- package/package.json +49 -4
package/README.md
CHANGED
|
@@ -1,3 +1,41 @@
|
|
|
1
|
-
#
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
# @hw-connector/client
|
|
2
|
+
|
|
3
|
+
Клиент HW agent (hw-connector) для браузера и Node.js 18+: ККТ, банковские терминалы, продажа одной операцией, печать
|
|
4
|
+
(PDF, HTML, этикетки), весы, дисплеи и события. TypeScript-типы в комплекте, без зависимостей.
|
|
5
|
+
|
|
6
|
+
```js
|
|
7
|
+
import {HwConnector, item, kopecks} from '@hw-connector/client';
|
|
8
|
+
|
|
9
|
+
const hw = new HwConnector(); // http://localhost:7790
|
|
10
|
+
|
|
11
|
+
const r = await hw.checkout({
|
|
12
|
+
registerId: 1, terminalId: 2,
|
|
13
|
+
receipt: {type: 'SELL', items: [item('Билет', kopecks(1500))], payments: [{type: 'CARD', amount: kopecks(1500)}]},
|
|
14
|
+
}, {idempotencyKey: 'order-123', onStage: s => console.log(s)}); // «Приложите карту к терминалу»
|
|
15
|
+
console.log(r.receipt.number, r.payment.paymentRef);
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
- **Операции** (`checkout`, `registers.*`, `terminals.*`, `print`, `labels.print`…) идут с ключом идемпотентности и ждут результат
|
|
19
|
+
(`Prefer: wait`, затем опрос). При обрыве связи запрос повторяется **с тем же ключом** — оплата и чек не пройдут дважды.
|
|
20
|
+
Свой ключ (`idempotencyKey: 'order-123'`) делает безопасным и повтор после перезагрузки страницы.
|
|
21
|
+
- **Ошибки** — `HwError` с кодом агента: `PAYMENT_CANCELLED`, `PAYMENT_NOT_CANCELLED`, `DEVICE_ERROR`, `VALIDATION_ERROR`,
|
|
22
|
+
`SITE_NOT_ALLOWED` (сайт ждёт разрешения кассира), `AGENT_UNAVAILABLE` (агент не запущен).
|
|
23
|
+
- **События**: `hw.events({barcode, weight, printer, operation, power, qr, card})` → функция отписки.
|
|
24
|
+
- **Печать**: `hw.print({urls | document | images | raw | html | htmlUrl, printer | device | role, options})`, `hw.printHtml(html)`,
|
|
25
|
+
`hw.printers.status()`, `hw.printers.capabilities(name)`, `hw.printers.jobs()`, `cancel`, `retry`.
|
|
26
|
+
- **Этикетки**: `hw.labels.print('price-zpl', {name, price, barcode}, {device: 4})`.
|
|
27
|
+
- **Весы**: `hw.scales.weight(3)`; **компьютер**: `hw.computer()` (machineId для привязки кассы к ПК).
|
|
28
|
+
- **Токен** агента: `new HwConnector({token})`; **подпись запросов** ключом сайта: `signer` — см. `examples/sign-server.mjs`.
|
|
29
|
+
|
|
30
|
+
- **ККТ целиком**: `registers.shift/openShift/closeShift/xReport/calculationState/receipt/correction/cashIn/cashOut/text/openCashDrawer`,
|
|
31
|
+
ФН — `registers.fnStatus/fnDocument/printFnDocument`, `registers.counters`.
|
|
32
|
+
- **Терминал**: `terminals.pay/cancel/refund/emergencyReversal/settlement/report/lastSlip`.
|
|
33
|
+
- **Дисплей и прочее**: `displays.show/clear/qr/get`, `openCashDrawer(id)`, `exchange(id, {hex})`, `qrUrl(text)`, `files.*`,
|
|
34
|
+
`emulators.list/setMode` (режимы ответа для автотестов), `operations.list/get`, `labels.get/render`, `printers.queue/clearQueue`.
|
|
35
|
+
|
|
36
|
+
Примеры: `examples/all-features.mjs` — все функции по порядку (на эмуляторах проходит целиком: `node examples/all-features.mjs`),
|
|
37
|
+
`examples/plain.html` (без сборки), `examples/react.tsx`, `examples/vue.vue`, `examples/sign-server.mjs`.
|
|
38
|
+
Общие правила API и клиенты для других языков — [../README.md](../README.md).
|
|
39
|
+
|
|
40
|
+
Сборка и тесты: `npm install && npm test` (TypeScript → `dist/`). Публикация в npm — `npm publish --access public`
|
|
41
|
+
(пакет пока не опубликован).
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
import type { CardPayment, CheckoutRequest, CheckoutResult, Computer, Device, Events, FiscalDocument, FnStatus, Label, MarkingCheckRequest, MarkingCheckResult, Operation, Operator, PrintJob, PrinterStatus, Receipt, TextLine, Weight } from './types.js';
|
|
2
|
+
export * from './types.js';
|
|
3
|
+
/**
|
|
4
|
+
* Подпись запроса для сайтов, чей ключ задан в агенте: строку подписывает сервер сайта закрытым ключом
|
|
5
|
+
* (SHA256withECDSA или SHA256withRSA) и возвращает подпись в base64. Закрытый ключ в браузер не попадает.
|
|
6
|
+
*/
|
|
7
|
+
export type Signer = (canonical: string) => Promise<string>;
|
|
8
|
+
export interface HwConnectorOptions {
|
|
9
|
+
/** Адрес агента; по умолчанию http://localhost:7790 */
|
|
10
|
+
baseUrl?: string;
|
|
11
|
+
/** Токен API агента (Authorization: Bearer) */
|
|
12
|
+
token?: string;
|
|
13
|
+
/** Подпись запросов ключом сайта */
|
|
14
|
+
signer?: Signer;
|
|
15
|
+
/** Свой fetch (Node < 18, тесты) */
|
|
16
|
+
fetch?: typeof fetch;
|
|
17
|
+
/** Сколько раз повторять запрос при обрыве связи (с тем же ключом идемпотентности) */
|
|
18
|
+
retries?: number;
|
|
19
|
+
/** Сколько ждать результат операции, секунд; по умолчанию 240 (терминалу нужно время) */
|
|
20
|
+
operationTimeoutSec?: number;
|
|
21
|
+
}
|
|
22
|
+
/** Ошибка агента или операции: code — машинный код (DEVICE_ERROR, PAYMENT_CANCELLED, VALIDATION_ERROR, SITE_NOT_ALLOWED…) */
|
|
23
|
+
export declare class HwError extends Error {
|
|
24
|
+
readonly code: string;
|
|
25
|
+
readonly status?: number;
|
|
26
|
+
readonly details?: unknown;
|
|
27
|
+
readonly operation?: Operation;
|
|
28
|
+
constructor(code: string, message: string, extra?: {
|
|
29
|
+
status?: number;
|
|
30
|
+
details?: unknown;
|
|
31
|
+
operation?: Operation;
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
export interface OperationCallOptions {
|
|
35
|
+
/** Свой ключ идемпотентности (например, номер заказа): повтор с ним вернёт ту же операцию */
|
|
36
|
+
idempotencyKey?: string;
|
|
37
|
+
/** Сколько ждать результат, секунд */
|
|
38
|
+
timeoutSec?: number;
|
|
39
|
+
/** Этапы для кассира: «Приложите карту к терминалу» */
|
|
40
|
+
onStage?: (stage: string, op: Operation) => void;
|
|
41
|
+
}
|
|
42
|
+
export declare class HwConnector {
|
|
43
|
+
readonly baseUrl: string;
|
|
44
|
+
private readonly token?;
|
|
45
|
+
private readonly signer?;
|
|
46
|
+
private readonly fetchFn;
|
|
47
|
+
private readonly retries;
|
|
48
|
+
private readonly operationTimeoutSec;
|
|
49
|
+
constructor(options?: HwConnectorOptions);
|
|
50
|
+
/**
|
|
51
|
+
* Запрос к API; ответ JSON. Ошибка HTTP — HwError с кодом из ответа агента (RFC 9457).
|
|
52
|
+
*/
|
|
53
|
+
request<T>(method: string, path: string, body?: unknown, headers?: Record<string, string>): Promise<T>;
|
|
54
|
+
/**
|
|
55
|
+
* Операция: POST с ключом идемпотентности и ожиданием до её окончания.
|
|
56
|
+
*/
|
|
57
|
+
operation<T = unknown>(path: string, body?: unknown, options?: OperationCallOptions): Promise<Operation<T>>;
|
|
58
|
+
/**
|
|
59
|
+
* Операция и сразу её результат; неудача — HwError с кодом операции (PAYMENT_CANCELLED, DEVICE_ERROR…).
|
|
60
|
+
*/
|
|
61
|
+
run<T = unknown>(path: string, body?: unknown, options?: OperationCallOptions): Promise<T>;
|
|
62
|
+
health(): Promise<{
|
|
63
|
+
status: string;
|
|
64
|
+
version: string;
|
|
65
|
+
startedAt: string;
|
|
66
|
+
power?: unknown;
|
|
67
|
+
}>;
|
|
68
|
+
devices(): Promise<Device[]>;
|
|
69
|
+
computer(): Promise<Computer>;
|
|
70
|
+
/** Агент запущен? */
|
|
71
|
+
available(): Promise<boolean>;
|
|
72
|
+
readonly registers: {
|
|
73
|
+
shift: (id: number) => Promise<{
|
|
74
|
+
status: "OPENED" | "CLOSED" | "EXPIRED";
|
|
75
|
+
number?: number;
|
|
76
|
+
}>;
|
|
77
|
+
openShift: (id: number, operator?: Operator) => Promise<FiscalDocument>;
|
|
78
|
+
closeShift: (id: number, operator?: Operator) => Promise<FiscalDocument>;
|
|
79
|
+
xReport: (id: number, operator?: Operator) => Promise<void>;
|
|
80
|
+
cashIn: (id: number, amount: number, operator?: Operator) => Promise<FiscalDocument>;
|
|
81
|
+
cashOut: (id: number, amount: number, operator?: Operator) => Promise<FiscalDocument>;
|
|
82
|
+
receipt: (id: number, receipt: Receipt, options?: OperationCallOptions) => Promise<FiscalDocument>;
|
|
83
|
+
correction: (id: number, receipt: Receipt, correction: {
|
|
84
|
+
type: "SELF" | "INSTRUCTION";
|
|
85
|
+
baseDate: string;
|
|
86
|
+
baseNumber?: string;
|
|
87
|
+
}) => Promise<FiscalDocument>;
|
|
88
|
+
text: (id: number, lines: TextLine[]) => Promise<void>;
|
|
89
|
+
openCashDrawer: (id: number) => Promise<void>;
|
|
90
|
+
/** Отчёт о текущем состоянии расчётов */
|
|
91
|
+
calculationState: (id: number, operator?: Operator) => Promise<FiscalDocument>;
|
|
92
|
+
counters: (id: number) => Promise<Record<string, unknown>>;
|
|
93
|
+
/** Срок ФН и непереданные в ОФД */
|
|
94
|
+
fnStatus: (id: number) => Promise<FnStatus>;
|
|
95
|
+
/** Документ из ФН по номеру: теги */
|
|
96
|
+
fnDocument: (id: number, number: number) => Promise<Record<string, unknown>>;
|
|
97
|
+
/** Копия документа из ФН на ленте */
|
|
98
|
+
printFnDocument: (id: number, number: number) => Promise<void>;
|
|
99
|
+
};
|
|
100
|
+
/** Продажа или возврат целиком: карта → чек → слип; если чек не напечатан, оплата отменяется */
|
|
101
|
+
checkout(request: CheckoutRequest, options?: OperationCallOptions): Promise<CheckoutResult>;
|
|
102
|
+
readonly marking: {
|
|
103
|
+
/** «Проверь код → можно продать»: Честный знак через ТС ПИоТ, локальный модуль или ГИС МТ */
|
|
104
|
+
check: (request: MarkingCheckRequest) => Promise<MarkingCheckResult>;
|
|
105
|
+
status: () => Promise<Record<string, unknown>>;
|
|
106
|
+
};
|
|
107
|
+
readonly terminals: {
|
|
108
|
+
pay: (id: number, amount: number, options?: OperationCallOptions) => Promise<CardPayment>;
|
|
109
|
+
refund: (id: number, amount: number, paymentRef: string) => Promise<CardPayment>;
|
|
110
|
+
cancel: (id: number, amount: number, paymentRef: string) => Promise<CardPayment>;
|
|
111
|
+
settlement: (id: number) => Promise<CardPayment>;
|
|
112
|
+
emergencyReversal: (id: number, amount: number, paymentRef?: string) => Promise<CardPayment>;
|
|
113
|
+
/** Отчёт терминала: краткий или полный */
|
|
114
|
+
report: (id: number, full?: boolean) => Promise<CardPayment>;
|
|
115
|
+
/** Последний слип — если касса его потеряла */
|
|
116
|
+
lastSlip: (id: number) => Promise<{
|
|
117
|
+
operationId: string;
|
|
118
|
+
type: string;
|
|
119
|
+
finishedAt: string;
|
|
120
|
+
slip: string[];
|
|
121
|
+
}>;
|
|
122
|
+
};
|
|
123
|
+
print(job: PrintJob, options?: OperationCallOptions): Promise<Record<string, unknown>>;
|
|
124
|
+
/** Напечатать HTML-страницу (как из браузера) */
|
|
125
|
+
printHtml(html: string, target?: Pick<PrintJob, 'printer' | 'device' | 'role' | 'copies' | 'options'>): Promise<Record<string, unknown>>;
|
|
126
|
+
readonly printers: {
|
|
127
|
+
list: () => Promise<{
|
|
128
|
+
name: string;
|
|
129
|
+
isDefault: boolean;
|
|
130
|
+
roles: string[];
|
|
131
|
+
}[]>;
|
|
132
|
+
status: () => Promise<PrinterStatus[]>;
|
|
133
|
+
capabilities: (name: string) => Promise<Record<string, unknown>>;
|
|
134
|
+
jobs: (limit?: number) => Promise<Operation<unknown>[]>;
|
|
135
|
+
cancel: (id: string) => Promise<Operation<unknown>>;
|
|
136
|
+
retry: (id: string) => Promise<unknown>;
|
|
137
|
+
/** Очередь принтера в Windows */
|
|
138
|
+
queue: (name: string) => Promise<Record<string, unknown>[]>;
|
|
139
|
+
clearQueue: (name: string) => Promise<Record<string, unknown>>;
|
|
140
|
+
};
|
|
141
|
+
readonly labels: {
|
|
142
|
+
list: () => Promise<Label[]>;
|
|
143
|
+
get: (name: string) => Promise<Label>;
|
|
144
|
+
/** Подставить данные, не печатая: готовые команды принтера */
|
|
145
|
+
render: (name: string, data: Record<string, string>) => Promise<{
|
|
146
|
+
text: string;
|
|
147
|
+
}>;
|
|
148
|
+
print: (name: string, data: Record<string, string>, target?: {
|
|
149
|
+
device?: number;
|
|
150
|
+
printer?: string;
|
|
151
|
+
role?: string;
|
|
152
|
+
copies?: number;
|
|
153
|
+
}) => Promise<unknown>;
|
|
154
|
+
};
|
|
155
|
+
readonly scales: {
|
|
156
|
+
weight: (id: number) => Promise<Weight>;
|
|
157
|
+
zero: (id: number) => Promise<void>;
|
|
158
|
+
};
|
|
159
|
+
readonly displays: {
|
|
160
|
+
show: (id: number, lines: string[]) => Promise<unknown>;
|
|
161
|
+
clear: (id: number) => Promise<unknown>;
|
|
162
|
+
/** QR-код на дисплее (оплата по СБП) */
|
|
163
|
+
qr: (id: number, payload: string, top?: string, bottom?: string) => Promise<unknown>;
|
|
164
|
+
get: (id: number) => Promise<Record<string, unknown>>;
|
|
165
|
+
};
|
|
166
|
+
/** Отдельный денежный ящик (не через ККТ) */
|
|
167
|
+
openCashDrawer(drawerId: number): Promise<unknown>;
|
|
168
|
+
/** Порт устройства (COM, TCP, HID): отправить байты и прочитать ответ */
|
|
169
|
+
exchange(channelId: number, request: {
|
|
170
|
+
hex?: string;
|
|
171
|
+
base64?: string;
|
|
172
|
+
text?: string;
|
|
173
|
+
charset?: string;
|
|
174
|
+
readTimeoutMs?: number;
|
|
175
|
+
maxBytes?: number;
|
|
176
|
+
until?: string;
|
|
177
|
+
[k: string]: unknown;
|
|
178
|
+
}): Promise<{
|
|
179
|
+
bytes: number;
|
|
180
|
+
hex: string;
|
|
181
|
+
base64: string;
|
|
182
|
+
text?: string;
|
|
183
|
+
crcValid?: boolean;
|
|
184
|
+
}>;
|
|
185
|
+
/** Адрес картинки PNG с QR-кодом — для <img src> */
|
|
186
|
+
qrUrl(text: string, size?: number): string;
|
|
187
|
+
/** Файлы в разрешённой папке агента (hw.files.enabled) */
|
|
188
|
+
readonly files: {
|
|
189
|
+
list: (dir?: string) => Promise<{
|
|
190
|
+
path: string;
|
|
191
|
+
size: number;
|
|
192
|
+
modified: string;
|
|
193
|
+
directory: boolean;
|
|
194
|
+
}[]>;
|
|
195
|
+
read: (path: string) => Promise<{
|
|
196
|
+
path: string;
|
|
197
|
+
text?: string;
|
|
198
|
+
base64?: string;
|
|
199
|
+
}>;
|
|
200
|
+
write: (path: string, content: {
|
|
201
|
+
text?: string;
|
|
202
|
+
base64?: string;
|
|
203
|
+
}) => Promise<unknown>;
|
|
204
|
+
delete: (path: string) => Promise<unknown>;
|
|
205
|
+
};
|
|
206
|
+
/** Режимы ответа эмуляторов — для разработки и автотестов: NO_PAPER, OFFLINE, DECLINED, TIMEOUT… */
|
|
207
|
+
readonly emulators: {
|
|
208
|
+
list: () => Promise<{
|
|
209
|
+
number: number;
|
|
210
|
+
type: string;
|
|
211
|
+
name: string;
|
|
212
|
+
mode: {
|
|
213
|
+
mode: string;
|
|
214
|
+
once: boolean;
|
|
215
|
+
modes: {
|
|
216
|
+
id: string;
|
|
217
|
+
title: string;
|
|
218
|
+
description: string;
|
|
219
|
+
}[];
|
|
220
|
+
};
|
|
221
|
+
}[]>;
|
|
222
|
+
setMode: (number: number, mode: string, once?: boolean) => Promise<unknown>;
|
|
223
|
+
};
|
|
224
|
+
/** Журнал операций */
|
|
225
|
+
readonly operations: {
|
|
226
|
+
list: (limit?: number) => Promise<Operation<unknown>[]>;
|
|
227
|
+
get: (id: string) => Promise<Operation<unknown>>;
|
|
228
|
+
};
|
|
229
|
+
/**
|
|
230
|
+
* Подписка на события агента: операции, сканер, весы, принтеры, питание. Браузер переподключается сам.
|
|
231
|
+
*
|
|
232
|
+
* @returns отписка
|
|
233
|
+
*/
|
|
234
|
+
events(handlers: Events, EventSourceImpl?: typeof EventSource): () => void;
|
|
235
|
+
}
|
|
236
|
+
/** Рубли → копейки без ошибок округления: 1499.9 → 149990 */
|
|
237
|
+
export declare function kopecks(rubles: number): number;
|
|
238
|
+
/** Позиция чека: цена в копейках, стоимость считается сама */
|
|
239
|
+
export declare function item(name: string, price: number, quantity?: number, extra?: Partial<import('./types.js').ReceiptItem>): import('./types.js').ReceiptItem;
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
// Клиент hw-connector: обёртка над REST /api/v1 и событиями (SSE) HW agent.
|
|
2
|
+
// Операции (чек, оплата, печать) идут с ключом идемпотентности: при обрыве связи запрос повторяется с тем же ключом,
|
|
3
|
+
// и агент не проведёт оплату и не пробьёт чек второй раз.
|
|
4
|
+
export * from './types.js';
|
|
5
|
+
/** Ошибка агента или операции: code — машинный код (DEVICE_ERROR, PAYMENT_CANCELLED, VALIDATION_ERROR, SITE_NOT_ALLOWED…) */
|
|
6
|
+
export class HwError extends Error {
|
|
7
|
+
constructor(code, message, extra = {}) {
|
|
8
|
+
super(message);
|
|
9
|
+
this.name = 'HwError';
|
|
10
|
+
this.code = code;
|
|
11
|
+
this.status = extra.status;
|
|
12
|
+
this.details = extra.details;
|
|
13
|
+
this.operation = extra.operation;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
function uuid() {
|
|
17
|
+
const c = globalThis.crypto;
|
|
18
|
+
if (c && 'randomUUID' in c) {
|
|
19
|
+
return c.randomUUID();
|
|
20
|
+
}
|
|
21
|
+
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, ch => {
|
|
22
|
+
const r = Math.random() * 16 | 0;
|
|
23
|
+
return (ch === 'x' ? r : (r & 0x3 | 0x8)).toString(16);
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
async function sha256Hex(body) {
|
|
27
|
+
const data = new TextEncoder().encode(body);
|
|
28
|
+
const digest = await globalThis.crypto.subtle.digest('SHA-256', data);
|
|
29
|
+
return Array.from(new Uint8Array(digest), b => b.toString(16).padStart(2, '0')).join('');
|
|
30
|
+
}
|
|
31
|
+
const finished = (s) => s === 'SUCCEEDED' || s === 'FAILED' || s === 'UNKNOWN';
|
|
32
|
+
export class HwConnector {
|
|
33
|
+
constructor(options = {}) {
|
|
34
|
+
// ---------- ККТ ----------
|
|
35
|
+
this.registers = {
|
|
36
|
+
shift: (id) => this.request('GET', `/api/v1/registers/${id}/shift`),
|
|
37
|
+
openShift: (id, operator) => this.run(`/api/v1/registers/${id}/shift/open`, { operator }),
|
|
38
|
+
closeShift: (id, operator) => this.run(`/api/v1/registers/${id}/shift/close`, { operator }),
|
|
39
|
+
xReport: (id, operator) => this.run(`/api/v1/registers/${id}/reports/x`, { operator }),
|
|
40
|
+
cashIn: (id, amount, operator) => this.run(`/api/v1/registers/${id}/cash/in`, { operator, amount }),
|
|
41
|
+
cashOut: (id, amount, operator) => this.run(`/api/v1/registers/${id}/cash/out`, { operator, amount }),
|
|
42
|
+
receipt: (id, receipt, options) => this.run(`/api/v1/registers/${id}/receipts`, receipt, options),
|
|
43
|
+
correction: (id, receipt, correction) => this.run(`/api/v1/registers/${id}/corrections`, { receipt, correction }),
|
|
44
|
+
text: (id, lines) => this.run(`/api/v1/registers/${id}/documents`, { lines }),
|
|
45
|
+
openCashDrawer: (id) => this.run(`/api/v1/registers/${id}/cash-drawer/open`),
|
|
46
|
+
/** Отчёт о текущем состоянии расчётов */
|
|
47
|
+
calculationState: (id, operator) => this.run(`/api/v1/registers/${id}/reports/calculation-state`, { operator }),
|
|
48
|
+
counters: (id) => this.request('GET', `/api/v1/registers/${id}/counters`),
|
|
49
|
+
/** Срок ФН и непереданные в ОФД */
|
|
50
|
+
fnStatus: (id) => this.request('GET', `/api/v1/registers/${id}/fn-status`),
|
|
51
|
+
/** Документ из ФН по номеру: теги */
|
|
52
|
+
fnDocument: (id, number) => this.request('GET', `/api/v1/registers/${id}/fn-documents/${number}`),
|
|
53
|
+
/** Копия документа из ФН на ленте */
|
|
54
|
+
printFnDocument: (id, number) => this.run(`/api/v1/registers/${id}/fn-documents/${number}/print`),
|
|
55
|
+
};
|
|
56
|
+
// ---------- маркировка ----------
|
|
57
|
+
this.marking = {
|
|
58
|
+
/** «Проверь код → можно продать»: Честный знак через ТС ПИоТ, локальный модуль или ГИС МТ */
|
|
59
|
+
check: (request) => this.request('POST', '/api/v1/marking/check', request),
|
|
60
|
+
status: () => this.request('GET', '/api/v1/marking/status'),
|
|
61
|
+
};
|
|
62
|
+
// ---------- терминал ----------
|
|
63
|
+
this.terminals = {
|
|
64
|
+
pay: (id, amount, options) => this.run(`/api/v1/terminals/${id}/payments`, { amount }, options),
|
|
65
|
+
refund: (id, amount, paymentRef) => this.run(`/api/v1/terminals/${id}/refunds`, { amount, paymentRef }),
|
|
66
|
+
cancel: (id, amount, paymentRef) => this.run(`/api/v1/terminals/${id}/cancellations`, { amount, paymentRef }),
|
|
67
|
+
settlement: (id) => this.run(`/api/v1/terminals/${id}/settlements`),
|
|
68
|
+
emergencyReversal: (id, amount, paymentRef) => this.run(`/api/v1/terminals/${id}/emergency-reversals`, { amount, paymentRef }),
|
|
69
|
+
/** Отчёт терминала: краткий или полный */
|
|
70
|
+
report: (id, full = false) => this.run(`/api/v1/terminals/${id}/reports?full=${full}`),
|
|
71
|
+
/** Последний слип — если касса его потеряла */
|
|
72
|
+
lastSlip: (id) => this.request('GET', `/api/v1/terminals/${id}/last-slip`),
|
|
73
|
+
};
|
|
74
|
+
this.printers = {
|
|
75
|
+
list: () => this.request('GET', '/api/v1/printers'),
|
|
76
|
+
status: () => this.request('GET', '/api/v1/printers/status'),
|
|
77
|
+
capabilities: (name) => this.request('GET', '/api/v1/printers/capabilities?name=' + encodeURIComponent(name)),
|
|
78
|
+
jobs: (limit = 50) => this.request('GET', '/api/v1/print-jobs?limit=' + limit),
|
|
79
|
+
cancel: (id) => this.request('POST', `/api/v1/print-jobs/${id}/cancel`),
|
|
80
|
+
retry: (id) => this.run(`/api/v1/print-jobs/${id}/retry`),
|
|
81
|
+
/** Очередь принтера в Windows */
|
|
82
|
+
queue: (name) => this.request('GET', '/api/v1/printers/jobs?name=' + encodeURIComponent(name)),
|
|
83
|
+
clearQueue: (name) => this.request('DELETE', '/api/v1/printers/jobs?name=' + encodeURIComponent(name)),
|
|
84
|
+
};
|
|
85
|
+
// ---------- этикетки, весы, дисплей ----------
|
|
86
|
+
this.labels = {
|
|
87
|
+
list: () => this.request('GET', '/api/v1/labels'),
|
|
88
|
+
get: (name) => this.request('GET', `/api/v1/labels/${encodeURIComponent(name)}`),
|
|
89
|
+
/** Подставить данные, не печатая: готовые команды принтера */
|
|
90
|
+
render: (name, data) => this.request('POST', `/api/v1/labels/${encodeURIComponent(name)}/render`, data),
|
|
91
|
+
print: (name, data, target = {}) => this.run(`/api/v1/labels/${encodeURIComponent(name)}/print`, { ...target, data }),
|
|
92
|
+
};
|
|
93
|
+
this.scales = {
|
|
94
|
+
weight: (id) => this.request('GET', `/api/v1/scales/${id}/weight`),
|
|
95
|
+
zero: (id) => this.run(`/api/v1/scales/${id}/zero`),
|
|
96
|
+
};
|
|
97
|
+
this.displays = {
|
|
98
|
+
show: (id, lines) => this.request('POST', `/api/v1/displays/${id}`, { lines }),
|
|
99
|
+
clear: (id) => this.request('POST', `/api/v1/displays/${id}`, { lines: [] }),
|
|
100
|
+
/** QR-код на дисплее (оплата по СБП) */
|
|
101
|
+
qr: (id, payload, top, bottom) => this.request('POST', `/api/v1/displays/${id}/qr`, { payload, top, bottom }),
|
|
102
|
+
get: (id) => this.request('GET', `/api/v1/displays/${id}`),
|
|
103
|
+
};
|
|
104
|
+
/** Файлы в разрешённой папке агента (hw.files.enabled) */
|
|
105
|
+
this.files = {
|
|
106
|
+
list: (dir = '') => this.request('GET', '/api/v1/files' + (dir ? '?dir=' + encodeURIComponent(dir) : '')),
|
|
107
|
+
read: (path) => this.request('GET', '/api/v1/files/content?path=' + encodeURIComponent(path)),
|
|
108
|
+
write: (path, content) => this.request('PUT', '/api/v1/files/content?path=' + encodeURIComponent(path), content),
|
|
109
|
+
delete: (path) => this.request('DELETE', '/api/v1/files?path=' + encodeURIComponent(path)),
|
|
110
|
+
};
|
|
111
|
+
/** Режимы ответа эмуляторов — для разработки и автотестов: NO_PAPER, OFFLINE, DECLINED, TIMEOUT… */
|
|
112
|
+
this.emulators = {
|
|
113
|
+
list: () => this.request('GET', '/api/v1/emulators'),
|
|
114
|
+
setMode: (number, mode, once = false) => this.request('PUT', `/api/v1/emulators/${number}`, { mode, once }),
|
|
115
|
+
};
|
|
116
|
+
/** Журнал операций */
|
|
117
|
+
this.operations = {
|
|
118
|
+
list: (limit = 50) => this.request('GET', '/api/v1/operations?limit=' + limit),
|
|
119
|
+
get: (id) => this.request('GET', '/api/v1/operations/' + id),
|
|
120
|
+
};
|
|
121
|
+
this.baseUrl = (options.baseUrl ?? 'http://localhost:7790').replace(/\/+$/, '');
|
|
122
|
+
this.token = options.token;
|
|
123
|
+
this.signer = options.signer;
|
|
124
|
+
const f = options.fetch ?? globalThis.fetch;
|
|
125
|
+
if (!f) {
|
|
126
|
+
throw new Error('Нет fetch: передайте options.fetch');
|
|
127
|
+
}
|
|
128
|
+
this.fetchFn = f.bind(globalThis);
|
|
129
|
+
this.retries = options.retries ?? 3;
|
|
130
|
+
this.operationTimeoutSec = options.operationTimeoutSec ?? 240;
|
|
131
|
+
}
|
|
132
|
+
// ---------- транспорт ----------
|
|
133
|
+
/**
|
|
134
|
+
* Запрос к API; ответ JSON. Ошибка HTTP — HwError с кодом из ответа агента (RFC 9457).
|
|
135
|
+
*/
|
|
136
|
+
async request(method, path, body, headers = {}) {
|
|
137
|
+
const text = body === undefined ? undefined : JSON.stringify(body);
|
|
138
|
+
const h = { ...headers };
|
|
139
|
+
if (text !== undefined) {
|
|
140
|
+
h['Content-Type'] = 'application/json';
|
|
141
|
+
}
|
|
142
|
+
if (this.token) {
|
|
143
|
+
h['Authorization'] = 'Bearer ' + this.token;
|
|
144
|
+
}
|
|
145
|
+
let lastError;
|
|
146
|
+
for (let attempt = 0; attempt <= this.retries; attempt++) {
|
|
147
|
+
// подпись заново на каждую попытку: агент не принимает одну подпись дважды
|
|
148
|
+
if (this.signer) {
|
|
149
|
+
const ts = String(Date.now());
|
|
150
|
+
const canonical = ts + '\n' + method.toUpperCase() + '\n' + path + '\n' + await sha256Hex(text ?? '');
|
|
151
|
+
h['X-HW-Timestamp'] = ts;
|
|
152
|
+
h['X-HW-Signature'] = await this.signer(canonical);
|
|
153
|
+
}
|
|
154
|
+
let res;
|
|
155
|
+
try {
|
|
156
|
+
res = await this.fetchFn(this.baseUrl + path, { method, headers: h, body: text });
|
|
157
|
+
}
|
|
158
|
+
catch (e) {
|
|
159
|
+
lastError = e;
|
|
160
|
+
// повторять безопасно только чтение и операции с ключом идемпотентности
|
|
161
|
+
if (method !== 'GET' && !h['Idempotency-Key']) {
|
|
162
|
+
break;
|
|
163
|
+
}
|
|
164
|
+
await new Promise(r => setTimeout(r, 500 * (attempt + 1)));
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
const raw = await res.text();
|
|
168
|
+
const data = raw ? safeJson(raw) : undefined;
|
|
169
|
+
if (!res.ok) {
|
|
170
|
+
const p = (data ?? {});
|
|
171
|
+
throw new HwError(p.code ?? 'HTTP_' + res.status, p.detail ?? p.title ?? ('Ошибка агента ' + res.status), { status: res.status, details: data });
|
|
172
|
+
}
|
|
173
|
+
return data;
|
|
174
|
+
}
|
|
175
|
+
throw new HwError('AGENT_UNAVAILABLE', 'Агент hw-connector недоступен по адресу ' + this.baseUrl
|
|
176
|
+
+ ': запущен ли он на этом компьютере?', { details: lastError });
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* Операция: POST с ключом идемпотентности и ожиданием до её окончания.
|
|
180
|
+
*/
|
|
181
|
+
async operation(path, body, options = {}) {
|
|
182
|
+
const key = options.idempotencyKey ?? uuid();
|
|
183
|
+
const deadline = Date.now() + (options.timeoutSec ?? this.operationTimeoutSec) * 1000;
|
|
184
|
+
let op = await this.request('POST', path, body ?? {}, { 'Idempotency-Key': key, 'Prefer': 'wait=25' });
|
|
185
|
+
let stage = op.stage;
|
|
186
|
+
if (stage && options.onStage) {
|
|
187
|
+
options.onStage(stage, op);
|
|
188
|
+
}
|
|
189
|
+
while (!finished(op.status)) {
|
|
190
|
+
if (Date.now() > deadline) {
|
|
191
|
+
throw new HwError('TIMEOUT', 'Операция не закончилась вовремя: ' + op.id, { operation: op });
|
|
192
|
+
}
|
|
193
|
+
op = await this.request('GET', '/api/v1/operations/' + op.id, undefined, { 'Prefer': 'wait=25' });
|
|
194
|
+
if (op.stage && op.stage !== stage && options.onStage) {
|
|
195
|
+
stage = op.stage;
|
|
196
|
+
options.onStage(stage, op);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
return op;
|
|
200
|
+
}
|
|
201
|
+
/**
|
|
202
|
+
* Операция и сразу её результат; неудача — HwError с кодом операции (PAYMENT_CANCELLED, DEVICE_ERROR…).
|
|
203
|
+
*/
|
|
204
|
+
async run(path, body, options = {}) {
|
|
205
|
+
const op = await this.operation(path, body, options);
|
|
206
|
+
if (op.status !== 'SUCCEEDED') {
|
|
207
|
+
const e = op.error ?? { code: op.status, message: 'Операция ' + op.status };
|
|
208
|
+
throw new HwError(e.code, e.message, { details: e.details, operation: op });
|
|
209
|
+
}
|
|
210
|
+
return op.result;
|
|
211
|
+
}
|
|
212
|
+
// ---------- агент ----------
|
|
213
|
+
health() {
|
|
214
|
+
return this.request('GET', '/api/v1/health');
|
|
215
|
+
}
|
|
216
|
+
devices() {
|
|
217
|
+
return this.request('GET', '/api/v1/devices');
|
|
218
|
+
}
|
|
219
|
+
computer() {
|
|
220
|
+
return this.request('GET', '/api/v1/system/computer');
|
|
221
|
+
}
|
|
222
|
+
/** Агент запущен? */
|
|
223
|
+
async available() {
|
|
224
|
+
try {
|
|
225
|
+
await this.health();
|
|
226
|
+
return true;
|
|
227
|
+
}
|
|
228
|
+
catch {
|
|
229
|
+
return false;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
/** Продажа или возврат целиком: карта → чек → слип; если чек не напечатан, оплата отменяется */
|
|
233
|
+
checkout(request, options) {
|
|
234
|
+
return this.run('/api/v1/checkouts', request, options);
|
|
235
|
+
}
|
|
236
|
+
// ---------- печать ----------
|
|
237
|
+
print(job, options) {
|
|
238
|
+
return this.run('/api/v1/print-jobs', job, options);
|
|
239
|
+
}
|
|
240
|
+
/** Напечатать HTML-страницу (как из браузера) */
|
|
241
|
+
printHtml(html, target = {}) {
|
|
242
|
+
return this.print({ ...target, html });
|
|
243
|
+
}
|
|
244
|
+
// ---------- ящик, порт, файлы, эмуляторы, журнал ----------
|
|
245
|
+
/** Отдельный денежный ящик (не через ККТ) */
|
|
246
|
+
openCashDrawer(drawerId) {
|
|
247
|
+
return this.request('POST', `/api/v1/cash-drawers/${drawerId}/open`);
|
|
248
|
+
}
|
|
249
|
+
/** Порт устройства (COM, TCP, HID): отправить байты и прочитать ответ */
|
|
250
|
+
exchange(channelId, request) {
|
|
251
|
+
return this.request('POST', `/api/v1/channels/${channelId}/exchange`, request);
|
|
252
|
+
}
|
|
253
|
+
/** Адрес картинки PNG с QR-кодом — для <img src> */
|
|
254
|
+
qrUrl(text, size = 300) {
|
|
255
|
+
return `${this.baseUrl}/api/v1/qr?text=${encodeURIComponent(text)}&size=${size}`;
|
|
256
|
+
}
|
|
257
|
+
// ---------- события ----------
|
|
258
|
+
/**
|
|
259
|
+
* Подписка на события агента: операции, сканер, весы, принтеры, питание. Браузер переподключается сам.
|
|
260
|
+
*
|
|
261
|
+
* @returns отписка
|
|
262
|
+
*/
|
|
263
|
+
events(handlers, EventSourceImpl = globalThis.EventSource) {
|
|
264
|
+
if (!EventSourceImpl) {
|
|
265
|
+
throw new Error('Нет EventSource: передайте реализацию вторым параметром (в Node — пакет eventsource)');
|
|
266
|
+
}
|
|
267
|
+
const url = this.baseUrl + '/api/v1/events' + (this.token ? '?access_token=' + encodeURIComponent(this.token) : '');
|
|
268
|
+
const es = new EventSourceImpl(url);
|
|
269
|
+
for (const [name, fn] of Object.entries(handlers)) {
|
|
270
|
+
if (name === 'error') {
|
|
271
|
+
es.addEventListener('error', fn);
|
|
272
|
+
}
|
|
273
|
+
else if (fn) {
|
|
274
|
+
es.addEventListener(name, (e) => fn(safeJson(e.data)));
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
return () => es.close();
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
function safeJson(s) {
|
|
281
|
+
try {
|
|
282
|
+
return JSON.parse(s);
|
|
283
|
+
}
|
|
284
|
+
catch {
|
|
285
|
+
return s;
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
// ---------- помощники для чека ----------
|
|
289
|
+
/** Рубли → копейки без ошибок округления: 1499.9 → 149990 */
|
|
290
|
+
export function kopecks(rubles) {
|
|
291
|
+
return Math.round(rubles * 100);
|
|
292
|
+
}
|
|
293
|
+
/** Позиция чека: цена в копейках, стоимость считается сама */
|
|
294
|
+
export function item(name, price, quantity = 1, extra = {}) {
|
|
295
|
+
return {
|
|
296
|
+
name, price, quantity, amount: Math.round(price * quantity),
|
|
297
|
+
vat: 'NONE', paymentMethod: 'FULL_PAYMENT', paymentObject: 'SERVICE', ...extra,
|
|
298
|
+
};
|
|
299
|
+
}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
export type OperationStatus = 'QUEUED' | 'RUNNING' | 'SUCCEEDED' | 'FAILED' | 'UNKNOWN';
|
|
2
|
+
export interface OperationError {
|
|
3
|
+
code: string;
|
|
4
|
+
message: string;
|
|
5
|
+
details?: unknown;
|
|
6
|
+
}
|
|
7
|
+
export interface Operation<T = unknown> {
|
|
8
|
+
id: string;
|
|
9
|
+
type: string;
|
|
10
|
+
idempotencyKey?: string;
|
|
11
|
+
resources?: string[];
|
|
12
|
+
status: OperationStatus;
|
|
13
|
+
stage?: string;
|
|
14
|
+
createdAt?: string;
|
|
15
|
+
startedAt?: string;
|
|
16
|
+
finishedAt?: string;
|
|
17
|
+
result?: T;
|
|
18
|
+
error?: OperationError;
|
|
19
|
+
warnings?: string[];
|
|
20
|
+
}
|
|
21
|
+
export interface Operator {
|
|
22
|
+
name: string;
|
|
23
|
+
inn?: string;
|
|
24
|
+
}
|
|
25
|
+
export type ReceiptType = 'SELL' | 'SELL_RETURN' | 'BUY' | 'BUY_RETURN';
|
|
26
|
+
export type Taxation = 'OSN' | 'USN_INCOME' | 'USN_INCOME_OUTCOME' | 'ESHN' | 'PATENT';
|
|
27
|
+
export type Vat = 'NONE' | 'VAT0' | 'VAT5' | 'VAT7' | 'VAT10' | 'VAT20' | 'VAT22' | 'VAT105' | 'VAT107' | 'VAT110' | 'VAT120' | 'VAT122';
|
|
28
|
+
export type PaymentMethod = 'FULL_PREPAYMENT' | 'PREPAYMENT' | 'ADVANCE' | 'FULL_PAYMENT' | 'PARTIAL_PAYMENT' | 'CREDIT' | 'CREDIT_PAYMENT';
|
|
29
|
+
export type PaymentObject = 'COMMODITY' | 'EXCISE' | 'JOB' | 'SERVICE' | 'GAMBLING_BET' | 'GAMBLING_PRIZE' | 'LOTTERY' | 'LOTTERY_PRIZE' | 'INTELLECTUAL_ACTIVITY' | 'PAYMENT' | 'AGENT_COMMISSION' | 'COMPOSITE' | 'ANOTHER';
|
|
30
|
+
export type PaymentType = 'CASH' | 'CARD' | 'PREPAID' | 'CREDIT' | 'OTHER';
|
|
31
|
+
export interface TextLine {
|
|
32
|
+
kind: 'TEXT' | 'PAIR' | 'SEPARATOR';
|
|
33
|
+
text?: string;
|
|
34
|
+
right?: string;
|
|
35
|
+
align?: 'LEFT' | 'CENTER' | 'RIGHT';
|
|
36
|
+
}
|
|
37
|
+
export interface ReceiptItem {
|
|
38
|
+
name: string;
|
|
39
|
+
/** Цена, копейки */
|
|
40
|
+
price: number;
|
|
41
|
+
quantity: number;
|
|
42
|
+
/** Стоимость = цена × количество, копейки */
|
|
43
|
+
amount: number;
|
|
44
|
+
vat: Vat;
|
|
45
|
+
paymentMethod: PaymentMethod;
|
|
46
|
+
paymentObject: PaymentObject;
|
|
47
|
+
department?: number;
|
|
48
|
+
/** Код маркировки (модуль маркировки) */
|
|
49
|
+
marking?: Marking;
|
|
50
|
+
}
|
|
51
|
+
/** Код маркировки позиции: агент проверит его в Честном знаке перед чеком, если нет industryProps */
|
|
52
|
+
export interface Marking {
|
|
53
|
+
/** Код со сканера, с разделителями GS (символ 29) */
|
|
54
|
+
code: string;
|
|
55
|
+
/** Дробное количество из упаковки, «1/10» */
|
|
56
|
+
fraction?: string;
|
|
57
|
+
/** Мера количества (тег 2108); по умолчанию PIECE */
|
|
58
|
+
measure?: string;
|
|
59
|
+
/** Результат проверки из marking.check() — тогда агент не проверяет код второй раз */
|
|
60
|
+
industryProps?: string;
|
|
61
|
+
}
|
|
62
|
+
export interface MarkingCheckRequest {
|
|
63
|
+
/** Чек, в который пойдут коды; по умолчанию SELL */
|
|
64
|
+
type?: Receipt['type'];
|
|
65
|
+
registerId?: number;
|
|
66
|
+
codes: {
|
|
67
|
+
code: string; /** Цена за единицу, копейки */
|
|
68
|
+
price?: number;
|
|
69
|
+
fraction?: string;
|
|
70
|
+
}[];
|
|
71
|
+
}
|
|
72
|
+
export interface MarkingCheckResult {
|
|
73
|
+
/** Все коды можно продать */
|
|
74
|
+
allowed: boolean;
|
|
75
|
+
items: {
|
|
76
|
+
code: string;
|
|
77
|
+
cis: string;
|
|
78
|
+
gtin: string;
|
|
79
|
+
allowed: boolean;
|
|
80
|
+
reasons: string[];
|
|
81
|
+
warnings: string[];
|
|
82
|
+
/** piot, lm, online, emulator; пусто — никто не ответил */
|
|
83
|
+
source?: string;
|
|
84
|
+
offline: boolean;
|
|
85
|
+
industryProps?: string;
|
|
86
|
+
info?: Record<string, unknown>;
|
|
87
|
+
}[];
|
|
88
|
+
}
|
|
89
|
+
export interface Receipt {
|
|
90
|
+
type: ReceiptType;
|
|
91
|
+
taxation?: Taxation;
|
|
92
|
+
operator?: Operator;
|
|
93
|
+
customer?: {
|
|
94
|
+
contact?: string;
|
|
95
|
+
name?: string;
|
|
96
|
+
inn?: string;
|
|
97
|
+
};
|
|
98
|
+
header?: TextLine[];
|
|
99
|
+
items: ReceiptItem[];
|
|
100
|
+
payments: {
|
|
101
|
+
type: PaymentType;
|
|
102
|
+
amount: number;
|
|
103
|
+
}[];
|
|
104
|
+
footer?: TextLine[];
|
|
105
|
+
electronic?: boolean;
|
|
106
|
+
senderEmail?: string;
|
|
107
|
+
settlementPlace?: string;
|
|
108
|
+
}
|
|
109
|
+
export interface FiscalDocument {
|
|
110
|
+
number?: number;
|
|
111
|
+
fiscalSign?: string;
|
|
112
|
+
shiftNumber?: number;
|
|
113
|
+
receiptNumber?: number;
|
|
114
|
+
fnNumber?: string;
|
|
115
|
+
registrationNumber?: string;
|
|
116
|
+
dateTime?: string;
|
|
117
|
+
total?: number;
|
|
118
|
+
qrCode?: string;
|
|
119
|
+
fnsUrl?: string;
|
|
120
|
+
/** Лента чека для просмотра на экране */
|
|
121
|
+
tape?: string[];
|
|
122
|
+
}
|
|
123
|
+
export interface CardPayment {
|
|
124
|
+
/** Ссылка на оплату — для возврата и отмены; формат как UniversalID ККМ-Сервера */
|
|
125
|
+
paymentRef: string;
|
|
126
|
+
amount: number;
|
|
127
|
+
card?: string;
|
|
128
|
+
rrn?: string;
|
|
129
|
+
authCode?: string;
|
|
130
|
+
terminalId?: string;
|
|
131
|
+
dateTime?: string;
|
|
132
|
+
slip?: string[];
|
|
133
|
+
}
|
|
134
|
+
export interface CheckoutRequest {
|
|
135
|
+
registerId: number;
|
|
136
|
+
terminalId?: number;
|
|
137
|
+
receipt: Receipt;
|
|
138
|
+
originalPaymentRef?: string;
|
|
139
|
+
printSlip?: boolean;
|
|
140
|
+
}
|
|
141
|
+
export interface CheckoutResult {
|
|
142
|
+
receipt: FiscalDocument;
|
|
143
|
+
payment?: CardPayment;
|
|
144
|
+
tape?: string[];
|
|
145
|
+
}
|
|
146
|
+
export interface PrintOptions {
|
|
147
|
+
pages?: string;
|
|
148
|
+
duplex?: 'ONE_SIDED' | 'LONG_EDGE' | 'SHORT_EDGE';
|
|
149
|
+
tray?: string;
|
|
150
|
+
color?: 'COLOR' | 'MONOCHROME';
|
|
151
|
+
dpi?: number;
|
|
152
|
+
nUp?: 1 | 2 | 4 | 6 | 9 | 16;
|
|
153
|
+
collate?: boolean;
|
|
154
|
+
orientation?: 'AUTO' | 'PORTRAIT' | 'LANDSCAPE';
|
|
155
|
+
rotation?: 0 | 90 | 270;
|
|
156
|
+
scaling?: 'ACTUAL' | 'FIT' | 'SHRINK';
|
|
157
|
+
paperWidthMm?: number;
|
|
158
|
+
paperHeightMm?: number;
|
|
159
|
+
marginsMm?: string;
|
|
160
|
+
jobName?: string;
|
|
161
|
+
}
|
|
162
|
+
/** Ровно один источник: urls, document, images, raw, text, html или htmlUrl */
|
|
163
|
+
export interface PrintJob {
|
|
164
|
+
urls?: string[];
|
|
165
|
+
/** PDF в base64 */
|
|
166
|
+
document?: string;
|
|
167
|
+
images?: string[];
|
|
168
|
+
/** Команды принтера в base64 */
|
|
169
|
+
raw?: string;
|
|
170
|
+
text?: TextLine[];
|
|
171
|
+
html?: string;
|
|
172
|
+
htmlUrl?: string;
|
|
173
|
+
printer?: string;
|
|
174
|
+
device?: number;
|
|
175
|
+
role?: 'tickets' | 'documents' | string;
|
|
176
|
+
copies?: number;
|
|
177
|
+
options?: PrintOptions;
|
|
178
|
+
expireAfterSec?: number;
|
|
179
|
+
}
|
|
180
|
+
export interface Device {
|
|
181
|
+
id: number;
|
|
182
|
+
code?: string;
|
|
183
|
+
kind: 'FISCAL_REGISTER' | 'PAYMENT_TERMINAL' | 'BARCODE_SCANNER' | 'PRINTER' | 'CASH_DRAWER' | 'CUSTOMER_DISPLAY' | 'CARD_READER' | 'CHANNEL' | 'SCALE' | 'SENSOR';
|
|
184
|
+
type: string;
|
|
185
|
+
name: string;
|
|
186
|
+
active: boolean;
|
|
187
|
+
info?: Record<string, unknown>;
|
|
188
|
+
}
|
|
189
|
+
export interface Weight {
|
|
190
|
+
deviceId: number;
|
|
191
|
+
/** Килограммы */
|
|
192
|
+
weight: number;
|
|
193
|
+
stable: boolean;
|
|
194
|
+
at: string;
|
|
195
|
+
}
|
|
196
|
+
export interface PrinterStatus {
|
|
197
|
+
name: string;
|
|
198
|
+
state: 'READY' | 'PRINTING' | 'OFFLINE' | 'ERROR' | 'UNKNOWN';
|
|
199
|
+
reasons?: string[];
|
|
200
|
+
jobs?: number;
|
|
201
|
+
at: string;
|
|
202
|
+
}
|
|
203
|
+
export interface Label {
|
|
204
|
+
name: string;
|
|
205
|
+
title?: string;
|
|
206
|
+
language: 'ZPL' | 'TSPL' | 'EZPL' | 'ESCPOS';
|
|
207
|
+
description?: string;
|
|
208
|
+
body: string;
|
|
209
|
+
sample?: Record<string, string>;
|
|
210
|
+
builtIn?: boolean;
|
|
211
|
+
fields?: string[];
|
|
212
|
+
}
|
|
213
|
+
export interface Computer {
|
|
214
|
+
machineId: string;
|
|
215
|
+
hostname: string;
|
|
216
|
+
user: string;
|
|
217
|
+
os: string;
|
|
218
|
+
osVersion: string;
|
|
219
|
+
arch: string;
|
|
220
|
+
agentVersion: string;
|
|
221
|
+
interfaces: {
|
|
222
|
+
name: string;
|
|
223
|
+
displayName: string;
|
|
224
|
+
mac?: string;
|
|
225
|
+
ipv4: string[];
|
|
226
|
+
ipv6: string[];
|
|
227
|
+
virtual: boolean;
|
|
228
|
+
}[];
|
|
229
|
+
}
|
|
230
|
+
/** События агента (SSE) */
|
|
231
|
+
export interface Events {
|
|
232
|
+
operation?: (op: Operation) => void;
|
|
233
|
+
barcode?: (scan: {
|
|
234
|
+
deviceId: number;
|
|
235
|
+
code: string;
|
|
236
|
+
at: string;
|
|
237
|
+
}) => void;
|
|
238
|
+
card?: (tap: {
|
|
239
|
+
deviceId: number;
|
|
240
|
+
uid: string;
|
|
241
|
+
at: string;
|
|
242
|
+
}) => void;
|
|
243
|
+
weight?: (w: Weight) => void;
|
|
244
|
+
printer?: (s: PrinterStatus) => void;
|
|
245
|
+
power?: (p: {
|
|
246
|
+
source: string;
|
|
247
|
+
batteryPercent?: number;
|
|
248
|
+
minutesLeft?: number;
|
|
249
|
+
}) => void;
|
|
250
|
+
qr?: (qr: {
|
|
251
|
+
deviceId: number;
|
|
252
|
+
payload?: string;
|
|
253
|
+
amount: number;
|
|
254
|
+
bank: string;
|
|
255
|
+
}) => void;
|
|
256
|
+
display?: (d: unknown) => void;
|
|
257
|
+
'site-request'?: (r: {
|
|
258
|
+
origin: string;
|
|
259
|
+
}) => void;
|
|
260
|
+
/** Соединение с агентом потеряно (браузер переподключится сам) */
|
|
261
|
+
error?: (e: Event) => void;
|
|
262
|
+
}
|
|
263
|
+
/** Состояние ФН: срок и непереданные в ОФД */
|
|
264
|
+
export interface FnStatus {
|
|
265
|
+
validUntil?: string;
|
|
266
|
+
daysLeft?: number;
|
|
267
|
+
notSent?: number;
|
|
268
|
+
firstNotSent?: number;
|
|
269
|
+
firstNotSentAt?: string;
|
|
270
|
+
}
|
package/dist/types.js
ADDED
package/package.json
CHANGED
|
@@ -1,6 +1,51 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hw-connector/client",
|
|
3
|
-
"version": "
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "2.7.0",
|
|
4
|
+
"description": "Клиент hw-connector для браузера и Node.js: ККТ, банковские терминалы, печать, этикетки, весы — REST и события агента с повторами и идемпотентностью",
|
|
5
|
+
"homepage": "https://hw-connector.ru/docs/api/agent/",
|
|
6
|
+
"bugs": {
|
|
7
|
+
"email": "support@bigbilet.ru"
|
|
8
|
+
},
|
|
9
|
+
"author": "BigBilet",
|
|
10
|
+
"type": "module",
|
|
11
|
+
"main": "./dist/index.js",
|
|
12
|
+
"types": "./dist/index.d.ts",
|
|
13
|
+
"exports": {
|
|
14
|
+
".": {
|
|
15
|
+
"types": "./dist/index.d.ts",
|
|
16
|
+
"import": "./dist/index.js"
|
|
17
|
+
}
|
|
18
|
+
},
|
|
19
|
+
"files": [
|
|
20
|
+
"dist",
|
|
21
|
+
"README.md"
|
|
22
|
+
],
|
|
23
|
+
"scripts": {
|
|
24
|
+
"build": "tsc -p tsconfig.json",
|
|
25
|
+
"test": "npm run build && node --test test/client.test.js",
|
|
26
|
+
"prepublishOnly": "npm test"
|
|
27
|
+
},
|
|
28
|
+
"keywords": [
|
|
29
|
+
"hw-connector",
|
|
30
|
+
"ккт",
|
|
31
|
+
"54-фз",
|
|
32
|
+
"атол",
|
|
33
|
+
"штрих-м",
|
|
34
|
+
"эквайринг",
|
|
35
|
+
"печать",
|
|
36
|
+
"zpl",
|
|
37
|
+
"весы",
|
|
38
|
+
"kkm-server",
|
|
39
|
+
"qz-tray"
|
|
40
|
+
],
|
|
41
|
+
"license": "UNLICENSED",
|
|
42
|
+
"engines": {
|
|
43
|
+
"node": ">=18"
|
|
44
|
+
},
|
|
45
|
+
"devDependencies": {
|
|
46
|
+
"typescript": "5.9.3"
|
|
47
|
+
},
|
|
48
|
+
"publishConfig": {
|
|
49
|
+
"access": "public"
|
|
50
|
+
}
|
|
51
|
+
}
|