@alizadefaig/pos-server 1.2.26 → 1.2.28
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/app/core/Context.js +170 -0
- package/app/core/auth/index.js +133 -0
- package/app/core/auth/license.js +45 -0
- package/app/core/auth/posMode.js +260 -0
- package/app/core/auth/sanctum.js +209 -0
- package/app/core/auth/scope.js +231 -0
- package/app/core/emitter.js +67 -0
- package/app/core/errors.js +18 -0
- package/app/core/guard.js +54 -0
- package/app/core/idempotency.js +196 -0
- package/app/core/index.js +374 -0
- package/app/core/models/Order.js +176 -0
- package/app/core/models/OrderProduct.js +249 -0
- package/app/core/repositories/EventRepository.js +51 -0
- package/app/core/repositories/OrderProductRepository.js +225 -0
- package/app/core/repositories/OrderRepository.js +274 -0
- package/app/core/repositories/OutboxRepository.js +52 -0
- package/app/core/repositories/PaymentRepository.js +43 -0
- package/app/core/repositories/PrinterRepository.js +179 -0
- package/app/core/repositories/ProductRelations.js +148 -0
- package/app/core/repositories/SnapshotRepository.js +197 -0
- package/app/core/resources/HallResource.js +54 -0
- package/app/core/resources/OrderResource.js +177 -0
- package/app/core/resources/ProductResource.js +186 -0
- package/app/core/resources/ShiftResource.js +42 -0
- package/app/core/resources/TeamResource.js +117 -0
- package/app/core/resources/casts.js +55 -0
- package/app/core/services/AuthService.js +468 -0
- package/app/core/services/InitService.js +279 -0
- package/app/core/services/KdsService.js +639 -0
- package/app/core/services/OrderProductService.js +309 -0
- package/app/core/services/OrderService.js +911 -0
- package/app/core/services/PrintService.js +715 -0
- package/app/core/services/PrinterAdminService.js +324 -0
- package/app/core/services/ReportService.js +581 -0
- package/app/core/services/ShiftService.js +199 -0
- package/app/core/services/StorageService.js +425 -0
- package/app/core/services/TelegramService.js +162 -0
- package/app/core/support/db.js +48 -0
- package/app/core/support/ids.js +9 -0
- package/app/core/support/imageData.js +135 -0
- package/app/core/support/images.js +92 -0
- package/app/core/support/lanHandoff.js +105 -0
- package/app/core/support/lang.js +98 -0
- package/app/core/support/lock.js +63 -0
- package/app/core/support/num.js +51 -0
- package/app/core/support/php.js +125 -0
- package/app/core/support/pin.js +34 -0
- package/app/core/support/receipts.js +278 -0
- package/app/core/support/rows.js +38 -0
- package/app/core/support/str.js +31 -0
- package/app/core/support/teamSettings.js +77 -0
- package/app/core/support/telegram.js +253 -0
- package/app/core/support/telegramReport.js +143 -0
- package/app/core/support/time.js +58 -0
- package/app/core/support/tz.js +80 -0
- package/app/core/support/unlock.js +113 -0
- package/app/core/timers.js +151 -0
- package/app/db/Database.js +197 -1
- package/app/db/Migrator.js +45 -0
- package/app/db/driver.js +248 -0
- package/app/db/migrations/001_snapshot.sql +214 -0
- package/app/db/migrations/002_orders.sql +190 -0
- package/app/db/migrations/003_idempotency.sql +18 -0
- package/app/db/migrations/004_shifts.sql +21 -0
- package/app/db/migrations/005_printers.sql +43 -0
- package/app/db/migrations/006_sync.sql +136 -0
- package/app/db/migrations/007_printer_mac.sql +4 -0
- package/app/db/migrations/008_kds.sql +94 -0
- package/app/db/migrations/009_product_categories.sql +20 -0
- package/app/db/migrations/010_printers_sync.sql +43 -0
- package/app/db/migrations/011_team_logos.sql +13 -0
- package/app/db/migrations/012_category_workshop.sql +4 -0
- package/app/monitor.js +185 -0
- package/app/printers/address.js +348 -0
- package/app/proxy.js +11 -3
- package/app/relay/masters.js +173 -0
- package/app/security.js +218 -0
- package/app/socket/auth.js +160 -0
- package/app/socket/request.js +87 -0
- package/app/socket/server.js +121 -19
- package/app/sync/bridge.js +162 -0
- package/app/sync/cloud.js +120 -0
- package/app/sync/config.js +185 -0
- package/app/sync/discovery.js +546 -0
- package/app/sync/index.js +1644 -0
- package/app/sync/kv.js +26 -0
- package/app/sync/lan.js +139 -0
- package/app/sync/lanDevices.js +177 -0
- package/app/sync/lanFront.js +431 -0
- package/app/sync/mirror.js +236 -0
- package/app/sync/push.js +324 -0
- package/app/sync/reconcile.js +161 -0
- package/app/sync/signal.js +202 -0
- package/app/sync/snapshot.js +189 -0
- package/app/sync/store.js +234 -0
- package/app/tcp.js +11 -2
- package/package.json +6 -3
- package/routes/common/api/auth.js +38 -0
- package/routes/common/api/kds.js +37 -0
- package/routes/common/api/orderProducts.js +81 -0
- package/routes/common/api/orders.js +176 -0
- package/routes/common/api/printersCore.js +35 -0
- package/routes/common/api/reports.js +29 -0
- package/routes/common/socket.js +15 -35
- package/routes/common/tax.js +29 -7
- package/routes/desktop/api/offline.js +146 -0
- package/routes/desktop/api/printers.js +288 -5
- package/routes/desktop/local.js +130 -0
- package/routes/server/api/login.js +37 -0
- package/routes/server/api/platform.js +31 -0
- package/routes/server/api/shifts.js +25 -0
- package/routes/server/master.js +24 -0
- package/server.js +72 -3
- package/utils.js +6 -0
- package/tests/usbDiagnose.js +0 -59
- package/tests/usbPrinterTest.js +0 -20
package/app/sync/kv.js
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Мелкое состояние синка в таблице kv: сессия терминала, курсор зеркала, blob app.init, времена.
|
|
3
|
+
* Значение — JSON. Запись зовётся только внутри синхронного блока store.block(...), чтение —
|
|
4
|
+
* откуда угодно (незакоммиченная строка ядра в kv не попадает, ядро kv не пишет).
|
|
5
|
+
*/
|
|
6
|
+
const get = (store, key, fallback = null) => {
|
|
7
|
+
const row = store.get('SELECT value FROM kv WHERE key = ?', [key]);
|
|
8
|
+
if (!row || row.value === null || row.value === undefined) {
|
|
9
|
+
return fallback;
|
|
10
|
+
}
|
|
11
|
+
try {
|
|
12
|
+
return JSON.parse(row.value);
|
|
13
|
+
} catch (e) {
|
|
14
|
+
return fallback;
|
|
15
|
+
}
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
const set = (store, key, value) => store.run(
|
|
19
|
+
`INSERT INTO kv (key, value, updated_at) VALUES (?, ?, datetime('now'))
|
|
20
|
+
ON CONFLICT (key) DO UPDATE SET value = excluded.value, updated_at = excluded.updated_at`,
|
|
21
|
+
[key, JSON.stringify(value === undefined ? null : value)]
|
|
22
|
+
);
|
|
23
|
+
|
|
24
|
+
const del = (store, key) => store.run('DELETE FROM kv WHERE key = ?', [key]);
|
|
25
|
+
|
|
26
|
+
module.exports = {get, set, del};
|
package/app/sync/lan.js
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
const {timingSafeEqual} = require('node:crypto');
|
|
2
|
+
const {isSyncEnabled, isLanFrontEnabled, lanKey, LAN_KEY_HEADER} = require('./config');
|
|
3
|
+
const lanDevices = require('./lanDevices');
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Защита мастера в зале. Мастер слушает 0.0.0.0, чтобы до него дошли ведомые терминалы, —
|
|
7
|
+
* значит, до него дойдёт и любое устройство в той же Wi-Fi. Всё, что пришло не с этой же
|
|
8
|
+
* машины, обязано предъявить общий ключ зала (CAFEPOS_LAN_KEY).
|
|
9
|
+
*
|
|
10
|
+
* Работает только при включённом синке и заданном ключе: облачный Node (публичный, без этих
|
|
11
|
+
* переменных) и касса без офлайна ведут себя как раньше.
|
|
12
|
+
*/
|
|
13
|
+
const isLoopback = (address) => {
|
|
14
|
+
// Адреса нет (сокет уже закрыт, нестандартный транспорт) — считаем чужим: пропускать
|
|
15
|
+
// «неизвестно кого» без ключа нельзя.
|
|
16
|
+
if (!address) {
|
|
17
|
+
return false;
|
|
18
|
+
}
|
|
19
|
+
const value = String(address).replace(/^::ffff:/, '');
|
|
20
|
+
return value === '::1' || value === 'localhost' || value.startsWith('127.');
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const keyMatches = (provided) => {
|
|
24
|
+
const expected = lanKey();
|
|
25
|
+
if (!expected || typeof provided !== 'string' || !provided) {
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
const a = Buffer.from(provided);
|
|
29
|
+
const b = Buffer.from(expected);
|
|
30
|
+
return a.length === b.length && timingSafeEqual(a, b);
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const guardActive = () => isSyncEnabled() && Boolean(lanKey());
|
|
34
|
+
|
|
35
|
+
// Поиск мастера (discovery.js): ведомый ещё не знает, кто мастер, и не шлёт ключ на неизвестный
|
|
36
|
+
// адрес. Ответ подписан ключом и сам его не раскрывает, поэтому путь открыт без ключа.
|
|
37
|
+
// /local/lan — опрос телефона, ушедшего на мастера по LAN (пора ли назад в облако): режим и
|
|
38
|
+
// доступность облака, ничего секретного.
|
|
39
|
+
const isKeylessPath = (req) => req.method === 'GET' && (req.path === '/local/whoami' || req.path === '/local/lan');
|
|
40
|
+
|
|
41
|
+
// Что телефону с токеном можно на мастере:
|
|
42
|
+
// - API и пустой /init фронта;
|
|
43
|
+
// - /tax — фискальный аппарат стоит в сети зала, а телефон на http-странице до него не дотянется
|
|
44
|
+
// сам. Мастер проксирует так же, как для своей страницы;
|
|
45
|
+
// - printers.print — только сетевой принтер в частной сети (проверка в routes/desktop/api/printers.js).
|
|
46
|
+
// Служебное (/local/session, /socket/request) и список принтеров самой кассы — только кассам зала
|
|
47
|
+
// с ключом.
|
|
48
|
+
const MASTER_ONLY_API = new Set(['/api/printers.get', '/api/printers.getusbprinters']);
|
|
49
|
+
const isDevicePath = (req) => {
|
|
50
|
+
const p = req.path.toLowerCase();
|
|
51
|
+
return (p.startsWith('/api/') && !MASTER_ONLY_API.has(p))
|
|
52
|
+
|| p === '/tax' || p.startsWith('/tax/')
|
|
53
|
+
|| (req.method === 'GET' && p === '/init');
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
const session = () => require('./index').session();
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Телефон официанта, ушедший с облака на мастера (LAN-переход): ключа зала у него нет, есть
|
|
60
|
+
* его обычный токен. Мастер узнаёт токен по реестру устройств заведения (lanDevices).
|
|
61
|
+
*/
|
|
62
|
+
const deviceFromRequest = (req) => {
|
|
63
|
+
if (!isLanFrontEnabled() || !isDevicePath(req)) {
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
const result = lanDevices.verify({
|
|
67
|
+
authorization: req.get('authorization'),
|
|
68
|
+
terminalId: req.get('terminal-id'),
|
|
69
|
+
companyId: req.get('company-id'),
|
|
70
|
+
teamId: req.get('team-id'),
|
|
71
|
+
}, session());
|
|
72
|
+
return result.ok ? result.device : null;
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
/** Express-middleware: чужой адрес без ключа — 403. */
|
|
76
|
+
const httpGuard = (req, res, next) => {
|
|
77
|
+
if (!guardActive() || isLoopback(req.socket && req.socket.remoteAddress) || isKeylessPath(req)) {
|
|
78
|
+
return next();
|
|
79
|
+
}
|
|
80
|
+
if (keyMatches(req.get(LAN_KEY_HEADER))) {
|
|
81
|
+
return next();
|
|
82
|
+
}
|
|
83
|
+
const device = deviceFromRequest(req);
|
|
84
|
+
if (device) {
|
|
85
|
+
req.lanDevice = device;
|
|
86
|
+
return next();
|
|
87
|
+
}
|
|
88
|
+
return res.status(403).json({success: false, message: 'Forbidden'});
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
const bearer = (token) => {
|
|
92
|
+
if (typeof token !== 'string' || !token) {
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
return /^Bearer\s/i.test(token) ? token : `Bearer ${token}`;
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
const first = (value) => (Array.isArray(value) ? value[0] : value);
|
|
99
|
+
|
|
100
|
+
/** Телефон по токену в handshake: auth.token + auth.team_id, query.company_id + query.terminal_id. */
|
|
101
|
+
const deviceFromHandshake = (handshake) => {
|
|
102
|
+
if (!isLanFrontEnabled()) {
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
105
|
+
const auth = handshake.auth || {};
|
|
106
|
+
const query = handshake.query || {};
|
|
107
|
+
const result = lanDevices.verify({
|
|
108
|
+
authorization: bearer(first(auth.token)),
|
|
109
|
+
terminalId: first(query.terminal_id),
|
|
110
|
+
companyId: first(query.company_id),
|
|
111
|
+
teamId: first(auth.team_id),
|
|
112
|
+
}, session());
|
|
113
|
+
return result.ok ? result.device : null;
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
/** socket.io-middleware (io.use): тот же ключ в handshake.auth.lan_key либо query.lan_key. */
|
|
117
|
+
const socketGuard = (socket, next) => {
|
|
118
|
+
if (!guardActive() || isLoopback(socket.handshake && socket.handshake.address)) {
|
|
119
|
+
return next();
|
|
120
|
+
}
|
|
121
|
+
const handshake = socket.handshake || {};
|
|
122
|
+
const provided = (handshake.auth && handshake.auth.lan_key) || (handshake.query && handshake.query.lan_key);
|
|
123
|
+
if (keyMatches(provided)) {
|
|
124
|
+
return next();
|
|
125
|
+
}
|
|
126
|
+
const device = deviceFromHandshake(handshake);
|
|
127
|
+
if (device) {
|
|
128
|
+
socket.data.lanDevice = device;
|
|
129
|
+
return next();
|
|
130
|
+
}
|
|
131
|
+
// Токена нет в реестре — возможно, телефон вошёл минуту назад и мастер ещё не обновил
|
|
132
|
+
// список. 'unauthorized' фронт повторяет сам (через 15 с), 'Forbidden' — нет.
|
|
133
|
+
return next(new Error(isLanFrontEnabled() && handshake.auth && handshake.auth.token ? 'unauthorized' : 'Forbidden'));
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
/** Заголовки запроса ведомого к мастеру. */
|
|
137
|
+
const lanHeaders = () => (lanKey() ? {[LAN_KEY_HEADER]: lanKey()} : {});
|
|
138
|
+
|
|
139
|
+
module.exports = {httpGuard, socketGuard, lanHeaders, isLoopback, keyMatches};
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
const {createHash, timingSafeEqual} = require('node:crypto');
|
|
2
|
+
const kv = require('./kv');
|
|
3
|
+
const config = require('./config');
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Реестр устройств заведения для LAN-перехода (config.isLanFrontEnabled): телефон официанта,
|
|
7
|
+
* ушедший с облака на мастера, предъявляет свой обычный токен Sanctum, и мастер узнаёт его
|
|
8
|
+
* без облака.
|
|
9
|
+
*
|
|
10
|
+
* Облако (sync.lanDevices, LanHandoffService::devices) отдаёт sha256 токенов, привязанных к
|
|
11
|
+
* терминалам заведения, — ровно то, что Sanctum хранит в БД. Сам токен облако не знает, мастер
|
|
12
|
+
* тоже: он только сверяет хеш. Копия лежит в kv (lan_devices) и переживает перезапуск кассы —
|
|
13
|
+
* без интернета её взять неоткуда.
|
|
14
|
+
*
|
|
15
|
+
* Новый токен мастер узнаёт на следующем обновлении (LAN_DEVICES_MS, 60 с) и только пока есть
|
|
16
|
+
* облако: телефон, вошедший уже после обрыва, мастер не пустит.
|
|
17
|
+
*/
|
|
18
|
+
const KEY = 'lan_devices';
|
|
19
|
+
|
|
20
|
+
const state = {
|
|
21
|
+
data: null,
|
|
22
|
+
tokens: new Map(),
|
|
23
|
+
at: null,
|
|
24
|
+
error: null,
|
|
25
|
+
lastTry: 0,
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const nowIso = () => new Date().toISOString();
|
|
29
|
+
|
|
30
|
+
const valid = (data) => Boolean(data)
|
|
31
|
+
&& typeof data === 'object'
|
|
32
|
+
&& typeof data.company_id === 'string'
|
|
33
|
+
&& Array.isArray(data.tokens)
|
|
34
|
+
&& data.terminals !== null
|
|
35
|
+
&& typeof data.terminals === 'object';
|
|
36
|
+
|
|
37
|
+
const apply = (data) => {
|
|
38
|
+
state.data = data;
|
|
39
|
+
state.tokens = new Map();
|
|
40
|
+
for (const token of data.tokens) {
|
|
41
|
+
if (token && token.id !== undefined && token.id !== null && typeof token.hash === 'string') {
|
|
42
|
+
state.tokens.set(String(token.id), token);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const clear = () => {
|
|
48
|
+
state.data = null;
|
|
49
|
+
state.tokens = new Map();
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
/** Копия из kv после перезапуска: без облака другой взять неоткуда. */
|
|
53
|
+
const load = (store) => {
|
|
54
|
+
if (state.data || !store) {
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
const saved = kv.get(store, KEY);
|
|
58
|
+
if (valid(saved)) {
|
|
59
|
+
apply(saved);
|
|
60
|
+
state.at = saved.fetched_at || null;
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
const due = () => Date.now() - state.lastTry >= config.lanFront().devices;
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Свежий список из облака. Облако недоступно — старый список остаётся (ради него всё и
|
|
68
|
+
* затевалось). 404/403 — раздача выключена или касса больше не мастер: старым токенам
|
|
69
|
+
* не верим, копию стираем.
|
|
70
|
+
*/
|
|
71
|
+
const refresh = async (cloud, store) => {
|
|
72
|
+
state.lastTry = Date.now();
|
|
73
|
+
let data;
|
|
74
|
+
try {
|
|
75
|
+
data = await cloud.call('sync.lanDevices', {});
|
|
76
|
+
} catch (e) {
|
|
77
|
+
if (e.name === 'CloudRequestError' && (e.status === 403 || e.status === 404)) {
|
|
78
|
+
clear();
|
|
79
|
+
state.at = null;
|
|
80
|
+
state.error = e.body && e.body.reason ? e.body.reason : `HTTP ${e.status}`;
|
|
81
|
+
await store.block((s) => kv.del(s, KEY));
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
state.error = e.message;
|
|
85
|
+
throw e;
|
|
86
|
+
}
|
|
87
|
+
if (!valid(data)) {
|
|
88
|
+
state.error = 'invalid_response';
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
const saved = {...data, fetched_at: nowIso()};
|
|
92
|
+
await store.block((s) => kv.set(s, KEY, saved));
|
|
93
|
+
apply(saved);
|
|
94
|
+
state.at = saved.fetched_at;
|
|
95
|
+
state.error = null;
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
const sameHex = (a, b) => {
|
|
99
|
+
if (typeof a !== 'string' || typeof b !== 'string' || a.length !== b.length) {
|
|
100
|
+
return false;
|
|
101
|
+
}
|
|
102
|
+
return timingSafeEqual(Buffer.from(a), Buffer.from(b));
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
const header = (value) => (Array.isArray(value) ? value[0] : value) || null;
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Узнать устройство по токену и заголовкам контекста. Токен — формата Sanctum `{id}|{plain}`,
|
|
109
|
+
* проверяется так же, как в Laravel: sha256(plain) против хеша строки с этим id. Терминал,
|
|
110
|
+
* заведение и команда обязаны совпасть с токеном и с сессией мастера — чужое заведение по
|
|
111
|
+
* своему токену сюда не попадёт.
|
|
112
|
+
*
|
|
113
|
+
* @returns {{ok: true, device: object} | {ok: false, reason: string}}
|
|
114
|
+
*/
|
|
115
|
+
const verify = ({authorization, terminalId, companyId, teamId}, session) => {
|
|
116
|
+
if (!state.data) {
|
|
117
|
+
return {ok: false, reason: 'no_devices'};
|
|
118
|
+
}
|
|
119
|
+
const match = /^Bearer\s+(\d+)\|(\S+)$/i.exec(header(authorization) || '');
|
|
120
|
+
if (!match) {
|
|
121
|
+
return {ok: false, reason: 'no_token'};
|
|
122
|
+
}
|
|
123
|
+
const token = state.tokens.get(match[1]);
|
|
124
|
+
const hash = createHash('sha256').update(match[2]).digest('hex');
|
|
125
|
+
if (!token || !sameHex(hash, token.hash)) {
|
|
126
|
+
return {ok: false, reason: 'unknown_token'};
|
|
127
|
+
}
|
|
128
|
+
if (token.expires_at && !(Date.parse(token.expires_at) > Date.now())) {
|
|
129
|
+
return {ok: false, reason: 'expired'};
|
|
130
|
+
}
|
|
131
|
+
terminalId = header(terminalId);
|
|
132
|
+
companyId = header(companyId);
|
|
133
|
+
teamId = header(teamId);
|
|
134
|
+
if (!terminalId || token.terminal_id !== terminalId || !state.data.terminals[terminalId]) {
|
|
135
|
+
return {ok: false, reason: 'terminal_mismatch'};
|
|
136
|
+
}
|
|
137
|
+
if (!session || !companyId || companyId !== session.company_id || companyId !== state.data.company_id) {
|
|
138
|
+
return {ok: false, reason: 'company_mismatch'};
|
|
139
|
+
}
|
|
140
|
+
if (session.team_id && teamId !== session.team_id) {
|
|
141
|
+
return {ok: false, reason: 'team_mismatch'};
|
|
142
|
+
}
|
|
143
|
+
return {
|
|
144
|
+
ok: true,
|
|
145
|
+
device: {
|
|
146
|
+
token_id: String(token.id),
|
|
147
|
+
terminal_id: terminalId,
|
|
148
|
+
company_id: companyId,
|
|
149
|
+
team_id: teamId,
|
|
150
|
+
},
|
|
151
|
+
};
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
/** TerminalResource терминала заведения — для app.init телефона. */
|
|
155
|
+
const terminal = (id) => (state.data && id && state.data.terminals[id]) || null;
|
|
156
|
+
|
|
157
|
+
/** Смена личности кассы: список чужого заведения больше не годится. */
|
|
158
|
+
const reset = async (store) => {
|
|
159
|
+
clear();
|
|
160
|
+
state.at = null;
|
|
161
|
+
state.error = null;
|
|
162
|
+
state.lastTry = 0;
|
|
163
|
+
if (store) {
|
|
164
|
+
await store.block((s) => kv.del(s, KEY));
|
|
165
|
+
}
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
const status = () => ({
|
|
169
|
+
at: state.at,
|
|
170
|
+
error: state.error,
|
|
171
|
+
company_id: state.data ? state.data.company_id : null,
|
|
172
|
+
generated_at: state.data ? state.data.generated_at || null : null,
|
|
173
|
+
tokens: state.tokens.size,
|
|
174
|
+
terminals: state.data ? Object.keys(state.data.terminals).length : 0,
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
module.exports = {load, due, refresh, verify, terminal, reset, status, _state: state};
|