@alizadefaig/pos-server 1.2.27 → 1.2.29
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.
|
@@ -42,11 +42,42 @@ const teamResource = (team, {currencySymbol = null, languageCode = null, urls =
|
|
|
42
42
|
};
|
|
43
43
|
};
|
|
44
44
|
|
|
45
|
+
const OFFLINE_MODES = ['off', 'standalone', 'hall'];
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Порт OfflineSetupService::forTerminal (Laravel): офлайн-режим кассы из суперадминки — то, что
|
|
49
|
+
* касса записывает в секцию offline своего config.json. null — заведение офлайном не управляет.
|
|
50
|
+
* Правила меняются в обоих местах сразу.
|
|
51
|
+
*
|
|
52
|
+
* @param {object} terminal строка terminals
|
|
53
|
+
* @param {?object} company строка companies (offline_mode, offline_master_terminal_id, offline_lan_key, offline_lan_front)
|
|
54
|
+
*/
|
|
55
|
+
const offlineConfig = (terminal, company) => {
|
|
56
|
+
const mode = company ? company.offline_mode : null;
|
|
57
|
+
if (!OFFLINE_MODES.includes(mode)) {
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
if (mode === 'off') {
|
|
61
|
+
return {enabled: false};
|
|
62
|
+
}
|
|
63
|
+
if (mode === 'hall' && company.offline_master_terminal_id && company.offline_lan_key) {
|
|
64
|
+
const isMaster = company.offline_master_terminal_id === terminal.id;
|
|
65
|
+
return {
|
|
66
|
+
enabled: true,
|
|
67
|
+
role: isMaster ? 'master' : 'follower',
|
|
68
|
+
master_url: isMaster ? null : 'auto',
|
|
69
|
+
lan_front: isMaster && Boolean(nullableBool(company.offline_lan_front)),
|
|
70
|
+
lan_key: company.offline_lan_key,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
return {enabled: true, role: 'standalone', master_url: null, lan_front: false};
|
|
74
|
+
};
|
|
75
|
+
|
|
45
76
|
/**
|
|
46
77
|
* @param {object} terminal строка terminals
|
|
47
|
-
* @param {{companyName: ?string, workshops: Array<{id, name}>, posMode: object}} rel
|
|
78
|
+
* @param {{companyName: ?string, workshops: Array<{id, name}>, posMode: object, company: ?object}} rel
|
|
48
79
|
*/
|
|
49
|
-
const terminalResource = (terminal, {companyName = null, workshops = [], posMode}) => ({
|
|
80
|
+
const terminalResource = (terminal, {companyName = null, workshops = [], posMode, company = null}) => ({
|
|
50
81
|
id: terminal.id,
|
|
51
82
|
name: terminal.name,
|
|
52
83
|
is_active: nullableBool(terminal.is_active),
|
|
@@ -58,6 +89,7 @@ const terminalResource = (terminal, {companyName = null, workshops = [], posMode
|
|
|
58
89
|
// Каст 'array', `?? []`.
|
|
59
90
|
settings: phpArray(terminal.settings) ?? [],
|
|
60
91
|
pos_mode: posMode,
|
|
92
|
+
offline: offlineConfig(terminal, company),
|
|
61
93
|
});
|
|
62
94
|
|
|
63
95
|
/**
|
|
@@ -82,4 +114,4 @@ const teamUserResource = (member, {user = null, role = null, scopes = {}, cashSh
|
|
|
82
114
|
shift: shiftResource(workShift),
|
|
83
115
|
});
|
|
84
116
|
|
|
85
|
-
module.exports = {teamResource, terminalResource, teamUserResource};
|
|
117
|
+
module.exports = {teamResource, terminalResource, teamUserResource, offlineConfig};
|
|
@@ -106,7 +106,7 @@ class InitService {
|
|
|
106
106
|
? this.db.get(`SELECT code FROM languages WHERE id = ?${this.alive()}`, [team.language_id])
|
|
107
107
|
: null,
|
|
108
108
|
terminal.company_id
|
|
109
|
-
? this.db.get(`SELECT
|
|
109
|
+
? this.db.get(`SELECT * FROM companies WHERE id = ?${this.alive()}`, [terminal.company_id])
|
|
110
110
|
: null,
|
|
111
111
|
this.db.all(
|
|
112
112
|
`SELECT pw.id, pw.name FROM product_workshops pw
|
|
@@ -129,6 +129,8 @@ class InitService {
|
|
|
129
129
|
companyName: company ? company.name : null,
|
|
130
130
|
workshops: terminalWorkshops,
|
|
131
131
|
posMode: mode,
|
|
132
|
+
// SELECT * выше: офлайн-колонок (offline_*) в локальной схеме кассы нет.
|
|
133
|
+
company,
|
|
132
134
|
}),
|
|
133
135
|
};
|
|
134
136
|
}
|
|
@@ -235,7 +235,7 @@ class OrderProductService {
|
|
|
235
235
|
|
|
236
236
|
async updateModifiers({orderProductId, modifiers}) {
|
|
237
237
|
const orderProduct = await this.findMutable(orderProductId, 'orderProducts.updateModifiers');
|
|
238
|
-
const [sets, , hash] = await OrderService.collectModifierSets(this.snapshot, modifiers);
|
|
238
|
+
const [sets, , hash] = await OrderService.collectModifierSets(this.snapshot, modifiers, this.context.locale());
|
|
239
239
|
|
|
240
240
|
await this.orderProducts.update(orderProductId, {modifications_sets_hash: hash});
|
|
241
241
|
await this.orderProducts.deleteModifierSets(orderProductId);
|
|
@@ -15,6 +15,7 @@ const {businessDay} = require('../support/tz');
|
|
|
15
15
|
const {uuid, isUuid} = require('../support/ids');
|
|
16
16
|
const {htmlspecialchars, md5, phpJsonEncode} = require('../support/str');
|
|
17
17
|
const {castRow} = require('../support/db');
|
|
18
|
+
const {translated} = require('../support/lang');
|
|
18
19
|
const {assertOrderMutable} = require('../guard');
|
|
19
20
|
const {withLock} = require('../support/lock');
|
|
20
21
|
|
|
@@ -239,7 +240,7 @@ class OrderService {
|
|
|
239
240
|
throw notFound('Product not found');
|
|
240
241
|
}
|
|
241
242
|
|
|
242
|
-
const [sets, , hash] = await OrderService.collectModifierSets(this.snapshot, modifierSets);
|
|
243
|
+
const [sets, , hash] = await OrderService.collectModifierSets(this.snapshot, modifierSets, this.context.locale());
|
|
243
244
|
|
|
244
245
|
// Повременная позиция уникальна по времени старта — её нельзя доливать в существующую.
|
|
245
246
|
const hasTimerModifier = await this.snapshot.hasTimerModifier(product.id);
|
|
@@ -861,11 +862,17 @@ class OrderService {
|
|
|
861
862
|
* Порядок ключей в элементе важен: хэш считается от json_encode этого массива и служит
|
|
862
863
|
* ключом склейки строк чека — переставь поля, и одинаковые позиции перестанут склеиваться.
|
|
863
864
|
*/
|
|
864
|
-
static async collectModifierSets(snapshot, modifierSets) {
|
|
865
|
+
static async collectModifierSets(snapshot, modifierSets, locale) {
|
|
865
866
|
let totalPrice = 0;
|
|
866
867
|
const data = [];
|
|
867
868
|
|
|
868
|
-
|
|
869
|
+
// Фронт шлёт modifier_sets_form как есть — объект по id модификатора (так его отдаёт и
|
|
870
|
+
// ProductResource). PHP-шный foreach обходит его как массив, for..of — нет.
|
|
871
|
+
const items = Array.isArray(modifierSets)
|
|
872
|
+
? modifierSets
|
|
873
|
+
: (modifierSets && typeof modifierSets === 'object' ? Object.values(modifierSets) : []);
|
|
874
|
+
|
|
875
|
+
for (const modifier of items) {
|
|
869
876
|
const modifierSet = await snapshot.findModifierSet(modifier.modifier_set_id);
|
|
870
877
|
if (!modifierSet) {
|
|
871
878
|
throw notFound('Modifier set not found');
|
|
@@ -884,7 +891,8 @@ class OrderService {
|
|
|
884
891
|
|
|
885
892
|
data.push({
|
|
886
893
|
modifier_set_type: modifierSet.type,
|
|
887
|
-
|
|
894
|
+
// title переводимый (Spatie): PHP отдаёт строку локали, а не JSON из колонки.
|
|
895
|
+
modifier_set_title: translated(productModifierSet.title, locale),
|
|
888
896
|
name: modifier.name,
|
|
889
897
|
total,
|
|
890
898
|
price,
|
package/app/sync/config.js
CHANGED
|
@@ -71,7 +71,7 @@ const intervals = () => ({
|
|
|
71
71
|
// модель M: как часто спрашивать у облака режим заведения (заодно это heartbeat кассы)
|
|
72
72
|
modePoll: num('OFFLINE_MODE_POLL_MS', 10000),
|
|
73
73
|
// выход терминала при недосланном: столько push досылает очередь, прежде чем токен отзовут
|
|
74
|
-
|
|
74
|
+
logoutDrain: num('OFFLINE_LOGOUT_DRAIN_MS', 30000),
|
|
75
75
|
});
|
|
76
76
|
|
|
77
77
|
const timeouts = () => ({
|
package/app/sync/index.js
CHANGED
|
@@ -100,9 +100,7 @@ const state = {
|
|
|
100
100
|
// чеки, которые облако просит перечитать (sync:pull)
|
|
101
101
|
pullIds: new Set(),
|
|
102
102
|
|
|
103
|
-
// выход терминала (logout):
|
|
104
|
-
// идёт отзыв токена — возврат на кассу (resume) в это время невозможен
|
|
105
|
-
revokeWaitFrom: null,
|
|
103
|
+
// выход терминала (logout): идёт отзыв токена — возврат на кассу (resume) в это время невозможен
|
|
106
104
|
revoking: false,
|
|
107
105
|
|
|
108
106
|
// сверка касса ↔ облако (reconcile.js): последний итог, расхождений подряд, сколько вылечено
|
|
@@ -386,10 +384,12 @@ const authoritativeEpoch = () => (
|
|
|
386
384
|
: null
|
|
387
385
|
);
|
|
388
386
|
|
|
389
|
-
|
|
387
|
+
// until — выход терминала: досылать, пока облако отвечает (без гистерезиса), не дольше этого срока.
|
|
388
|
+
const runPush = async ({until = null} = {}) => {
|
|
390
389
|
let rounds = 0;
|
|
391
390
|
reconcilePoison();
|
|
392
|
-
|
|
391
|
+
const going = () => (until === null ? state.online : state.oks > 0 && Date.now() < until);
|
|
392
|
+
while (refreshPending() > 0 && rounds < MAX_PUSH_ROUNDS && going()) {
|
|
393
393
|
rounds += 1;
|
|
394
394
|
const epoch = authoritativeEpoch();
|
|
395
395
|
let result;
|
|
@@ -1244,7 +1244,6 @@ const setSession = async (input) => {
|
|
|
1244
1244
|
// Фронт снова прислал сессию — значит, терминал вошёл обратно (resume).
|
|
1245
1245
|
delete state.session.parked;
|
|
1246
1246
|
}
|
|
1247
|
-
state.revokeWaitFrom = null;
|
|
1248
1247
|
return {success: true, mode: state.mode};
|
|
1249
1248
|
};
|
|
1250
1249
|
|
|
@@ -1260,7 +1259,6 @@ const clearSession = async () => {
|
|
|
1260
1259
|
}
|
|
1261
1260
|
stopComponents();
|
|
1262
1261
|
state.session = null;
|
|
1263
|
-
state.revokeWaitFrom = null;
|
|
1264
1262
|
await state.store.block((s) => kv.del(s, 'session'));
|
|
1265
1263
|
updateMode();
|
|
1266
1264
|
return {success: true};
|
|
@@ -1273,12 +1271,12 @@ const clearSession = async () => {
|
|
|
1273
1271
|
* (auth.logoutApp) и стирал сессию синка: без интернета выход падал, а недосланное
|
|
1274
1272
|
* ждало следующего входа, которого без интернета не бывает (app.auth — только облако).
|
|
1275
1273
|
*
|
|
1276
|
-
* Теперь выход «паркует» сессию: фронт свою авторизацию стирает, а синк токен
|
|
1277
|
-
*
|
|
1278
|
-
*
|
|
1279
|
-
* (
|
|
1280
|
-
* следующим входом в это заведение
|
|
1281
|
-
* (resume) — это единственный вход без облака.
|
|
1274
|
+
* Теперь выход «паркует» сессию: фронт свою авторизацию стирает, а синк держит токен до
|
|
1275
|
+
* первого ответа облака. Облако ответило (гистерезис связи не ждём) — синк досылает очередь не
|
|
1276
|
+
* дольше OFFLINE_LOGOUT_DRAIN_MS и отзывает токен в любом случае, в том числе на мастере модели M
|
|
1277
|
+
* (заведение он после этого не обслуживает). Недосланное остаётся помеченным и уйдёт со
|
|
1278
|
+
* следующим входом в это заведение на этой кассе. До отзыва на кассу можно вернуться по PIN
|
|
1279
|
+
* сотрудника (resume) — это единственный вход без облака.
|
|
1282
1280
|
*/
|
|
1283
1281
|
|
|
1284
1282
|
const initNames = () => {
|
|
@@ -1324,8 +1322,11 @@ const revokeToken = async (client, data = {}, timeout = config.timeouts().api) =
|
|
|
1324
1322
|
}
|
|
1325
1323
|
};
|
|
1326
1324
|
|
|
1325
|
+
// Облако ответило на последнюю проверку связи: для выхода гистерезис не ждём.
|
|
1326
|
+
const cloudAnswers = () => state.oks > 0;
|
|
1327
|
+
|
|
1327
1328
|
const canRevokeParked = () => Boolean(state.session && state.session.parked && state.cloud)
|
|
1328
|
-
&&
|
|
1329
|
+
&& cloudAnswers() && !state.revoking;
|
|
1329
1330
|
|
|
1330
1331
|
/** Отзыв токена запаркованной сессии. Вернулись на кассу, пока шёл запрос, — сессию не трогаем. */
|
|
1331
1332
|
const revokeParked = async (timeout) => {
|
|
@@ -1347,16 +1348,20 @@ const revokeParked = async (timeout) => {
|
|
|
1347
1348
|
|
|
1348
1349
|
const runRevoke = async () => {
|
|
1349
1350
|
if (!canRevokeParked()) {
|
|
1350
|
-
state.revokeWaitFrom = null;
|
|
1351
1351
|
return;
|
|
1352
1352
|
}
|
|
1353
|
-
if (refreshPending() > 0) {
|
|
1354
|
-
|
|
1355
|
-
if (
|
|
1353
|
+
if (!isFollower() && hasInit() && refreshPending() > 0) {
|
|
1354
|
+
await step('logout push', () => runPush({until: Date.now() + config.intervals().logoutDrain}));
|
|
1355
|
+
if (!canRevokeParked()) {
|
|
1356
1356
|
return;
|
|
1357
1357
|
}
|
|
1358
|
-
|
|
1359
|
-
|
|
1358
|
+
if (refreshPending() > 0) {
|
|
1359
|
+
console.warn(`[sync] logout: ${state.pending} unsent record(s) left — revoking the token anyway, `
|
|
1360
|
+
+ 'they wait for the next login to this company');
|
|
1361
|
+
}
|
|
1362
|
+
}
|
|
1363
|
+
if (isServingAsMaster()) {
|
|
1364
|
+
console.warn('[sync] terminal logged out on the master — the company is not served after the token is revoked');
|
|
1360
1365
|
}
|
|
1361
1366
|
await revokeParked();
|
|
1362
1367
|
};
|
|
@@ -1376,7 +1381,7 @@ const runLogoutQueue = async () => {
|
|
|
1376
1381
|
};
|
|
1377
1382
|
|
|
1378
1383
|
const runLogout = async () => {
|
|
1379
|
-
if (
|
|
1384
|
+
if (cloudAnswers() && state.store && (kv.get(state.store, 'logout_queue') || []).length) {
|
|
1380
1385
|
await step('logout queue', runLogoutQueue);
|
|
1381
1386
|
}
|
|
1382
1387
|
await step('revoke', runRevoke);
|
|
@@ -1402,9 +1407,7 @@ const logout = async ({token} = {}) => {
|
|
|
1402
1407
|
if (canRevokeParked() && refreshPending() === 0 && await revokeParked(REVOKE_TIMEOUT_MS)) {
|
|
1403
1408
|
return {success: true, handled: true, revoked: true};
|
|
1404
1409
|
}
|
|
1405
|
-
|
|
1406
|
-
console.warn('[sync] terminal logged out on the master — the company is still served, token kept');
|
|
1407
|
-
}
|
|
1410
|
+
// Очередь не пуста или облако не ответило — досылка и отзыв идут в цикле сразу же.
|
|
1408
1411
|
schedule(0);
|
|
1409
1412
|
return {success: true, handled: true, parked: true, pending: state.pending, master: isServingAsMaster()};
|
|
1410
1413
|
};
|
|
@@ -1450,7 +1453,6 @@ const resume = async ({code} = {}) => {
|
|
|
1450
1453
|
throw busy();
|
|
1451
1454
|
}
|
|
1452
1455
|
delete session.parked;
|
|
1453
|
-
state.revokeWaitFrom = null;
|
|
1454
1456
|
await state.store.block((s) => kv.set(s, 'session', {...session}));
|
|
1455
1457
|
log('terminal logged back in by PIN');
|
|
1456
1458
|
return {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@alizadefaig/pos-server",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.29",
|
|
4
4
|
"description": "Server in express js with Proxy to Main server if need and will work with Electron JS",
|
|
5
5
|
"homepage": "https://gitlab.com/faigalizade/pos-server#readme",
|
|
6
6
|
"bugs": {
|