@alizadefaig/pos-server 1.2.33 → 1.2.35

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.
@@ -85,7 +85,7 @@ class OrderResource {
85
85
  shift_id: order.shift_id,
86
86
  slot_index: order.slot_index,
87
87
  takeaway: null,
88
- delivery: null,
88
+ delivery: order.data?.delivery ?? null,
89
89
  activities: [],
90
90
  payments: order.payments,
91
91
  products: order.products.map((item) => this.orderProduct(item, rel)),
@@ -21,23 +21,30 @@ const {nullableBool, modelArray} = require('./casts');
21
21
  * @property {Map<string, object>} categories id → product_categories (для мутатора цеха)
22
22
  * @property {?Map<string, string>} urls id картинки → URL; null — таблицы images нет (касса)
23
23
  * @property {string} locale
24
+ * @property {?import('../support/prices').PriceBook} [prices] цены филиала/источника — только в
25
+ * справочнике кассы (InitService::products); у товаров внутри чеков — базовые
24
26
  */
25
27
 
26
- const modifierSetModifierResource = (modifier, {urls, locale}) => ({
28
+ /** price — цена филиала кассы; source_prices — { order_source_id: цена } или null. */
29
+ const priceOf = (id, base, rel) => (rel.prices ? rel.prices.branch(id, base) : money(base));
30
+ const sourcePricesOf = (id, rel) => (rel.prices ? rel.prices.sourcePrices(id) : null);
31
+
32
+ const modifierSetModifierResource = (modifier, rel) => ({
27
33
  id: modifier.id,
28
34
  product_id: modifier.product_id,
29
35
  type: modifier.type,
30
- name: trans(modifier.name, locale),
36
+ name: trans(modifier.name, rel.locale),
31
37
  // Каст 'boolean': в SQLite 0/1.
32
38
  is_without_debit: nullableBool(modifier.is_without_debit),
33
39
  min: modifier.min,
34
40
  max: modifier.max,
35
41
  // UnitableCast отдаёт значение как есть — numeric строкой.
36
42
  gross: modifier.gross ?? null,
37
- image: castImage(urls, modifier.image),
43
+ image: castImage(rel.urls, modifier.image),
38
44
  default_count: modifier.default_count,
39
45
  default: nullableBool(modifier.default),
40
- price: money(modifier.price),
46
+ price: priceOf(modifier.id, modifier.price, rel),
47
+ source_prices: sourcePricesOf(modifier.id, rel),
41
48
  });
42
49
 
43
50
  const productModifierSetResource = (item, rel) => {
@@ -86,7 +93,8 @@ const modifierSetsForm = (sets, rel) => {
86
93
  max: modifier.max,
87
94
  min: modifier.min,
88
95
  total: total !== null && total !== undefined && Number(total) < 0 ? total : 0,
89
- price: money(modifier.price),
96
+ price: priceOf(modifier.id, modifier.price, rel),
97
+ source_prices: sourcePricesOf(modifier.id, rel),
90
98
  modifier_set_modifier_id: modifier.id,
91
99
  modifier_set_id: item.modifier_set_id,
92
100
  };
@@ -108,7 +116,8 @@ const productResource = (product, rel, top = true) => {
108
116
  full_name: trans(product.full_name, rel.locale),
109
117
  name: trans(product.name, rel.locale),
110
118
  color: product.color ?? null,
111
- price: round2(money(product.price)),
119
+ price: round2(priceOf(product.id, product.price, rel)),
120
+ source_prices: sourcePricesOf(product.id, rel),
112
121
  };
113
122
  // whenLoaded: у модификаций ключей modifications и modifier_sets нет вовсе.
114
123
  if (top) {
@@ -12,6 +12,7 @@ const pin = require('../support/pin');
12
12
  const {HttpError} = require('../errors');
13
13
  const {toBool} = require('../support/num');
14
14
  const {companiesFor} = require('../support/terminalAccess');
15
+ const prices = require('../support/prices');
15
16
 
16
17
  /**
17
18
  * Облачные справочники POS: порт AuthController::init и соседних чтений (ProductService,
@@ -187,6 +188,8 @@ class InitService {
187
188
  return [];
188
189
  }
189
190
  const rel = await new ProductRelations(this.db, this.locale).load(products);
191
+ // Цены филиала кассы и источников заказа (support/prices.js).
192
+ rel.prices = await prices.forTeam(this.db, teamId, this.context.companyId);
190
193
  return products.map((product) => productResource(product, rel));
191
194
  }
192
195
 
@@ -241,7 +241,8 @@ class OrderProductService {
241
241
 
242
242
  async updateModifiers({orderProductId, modifiers}) {
243
243
  const orderProduct = await this.findMutable(orderProductId, 'orderProducts.updateModifiers');
244
- const [sets, , hash] = await OrderService.collectModifierSets(this.snapshot, modifiers, this.context.locale());
244
+ const order = await this.orders.findRow(orderProduct.order_id);
245
+ const [sets, , hash] = await OrderService.collectModifierSets(this.snapshot, modifiers, this.context.locale(), order);
245
246
 
246
247
  await this.orderProducts.update(orderProductId, {modifications_sets_hash: hash});
247
248
  await this.orderProducts.deleteModifierSets(orderProductId);
@@ -19,6 +19,8 @@ const {castRow} = require('../support/db');
19
19
  const {translated} = require('../support/lang');
20
20
  const {assertOrderMutable} = require('../guard');
21
21
  const {withLock} = require('../support/lock');
22
+ const prices = require('../support/prices');
23
+ const {normalizeDelivery} = require('../support/delivery');
22
24
 
23
25
  const TYPE_TIMER = OrderProduct.TYPE_TIMER;
24
26
  const TYPE_EXTRA = OrderProduct.TYPE_EXTRA;
@@ -150,7 +152,7 @@ class OrderService {
150
152
 
151
153
  // ---------------------------------------------------------------- чек
152
154
 
153
- async create({orderSourceId, tableId, guestCount = 1, hallId = null, servingId = null, id = null, slotIndex = 1}) {
155
+ async create({orderSourceId, tableId, guestCount = 1, hallId = null, servingId = null, id = null, slotIndex = 1, delivery = null}) {
154
156
  const context = this.context;
155
157
 
156
158
  let orderSource = await this.snapshot.findOrderSource(context.teamId, orderSourceId);
@@ -220,6 +222,8 @@ class OrderService {
220
222
  global_number: numbers.globalNumber,
221
223
  number: numbers.number,
222
224
  status: 'created',
225
+ // Доставка: адрес/телефон/имя (OrderService::normalizeDelivery в Laravel).
226
+ data: (delivery = normalizeDelivery(delivery)) ? {delivery} : null,
223
227
  });
224
228
  });
225
229
 
@@ -241,7 +245,7 @@ class OrderService {
241
245
  throw notFound('Product not found');
242
246
  }
243
247
 
244
- const [sets, , hash] = await OrderService.collectModifierSets(this.snapshot, modifierSets, this.context.locale());
248
+ const [sets, , hash] = await OrderService.collectModifierSets(this.snapshot, modifierSets, this.context.locale(), order);
245
249
 
246
250
  // Повременная позиция уникальна по времени старта — её нельзя доливать в существующую.
247
251
  const hasTimerModifier = await this.snapshot.hasTimerModifier(product.id);
@@ -261,7 +265,9 @@ class OrderService {
261
265
  order_id: order.id,
262
266
  product_id: product.id,
263
267
  guest_number: guestNumber,
264
- price: money(product.price),
268
+ // Цена филиала/источника чека (support/prices.js), иначе базовая.
269
+ price: money((await prices.forItems(this.db, {productIds: [product.id]}, order.company_id))
270
+ .resolve(product.id, product.price, order.order_source_id)),
265
271
  added_at: fromTimestamp(context.timestamp),
266
272
  team_id: context.teamId,
267
273
  company_id: context.companyId,
@@ -898,7 +904,7 @@ class OrderService {
898
904
  * Порядок ключей в элементе важен: хэш считается от json_encode этого массива и служит
899
905
  * ключом склейки строк чека — переставь поля, и одинаковые позиции перестанут склеиваться.
900
906
  */
901
- static async collectModifierSets(snapshot, modifierSets, locale) {
907
+ static async collectModifierSets(snapshot, modifierSets, locale, order = null) {
902
908
  let totalPrice = 0;
903
909
  const data = [];
904
910
 
@@ -908,6 +914,11 @@ class OrderService {
908
914
  ? modifierSets
909
915
  : (modifierSets && typeof modifierSets === 'object' ? Object.values(modifierSets) : []);
910
916
 
917
+ // Касса шлёт цену из справочника — своя цена филиала/источника чека её перекрывает.
918
+ const book = order && items.length
919
+ ? await prices.forItems(snapshot.db, {modifierIds: items.map((item) => item && item.modifier_set_modifier_id)}, order.company_id)
920
+ : null;
921
+
911
922
  for (const modifier of items) {
912
923
  const modifierSet = await snapshot.findModifierSet(modifier.modifier_set_id);
913
924
  if (!modifierSet) {
@@ -918,7 +929,10 @@ class OrderService {
918
929
  throw notFound('Product modifier set not found');
919
930
  }
920
931
 
921
- const price = toFloat(modifier.price);
932
+ const modifierId = modifier.modifier_set_modifier_id;
933
+ const price = book && modifierId && book.overrides(modifierId, order.order_source_id)
934
+ ? book.resolve(modifierId, modifier.price, order.order_source_id)
935
+ : toFloat(modifier.price);
922
936
  const quantity = modifier.quantity;
923
937
  if (quantity === 0) {
924
938
  continue;
@@ -10,6 +10,7 @@ const {uuid} = require('../support/ids');
10
10
  const {notFound} = require('../errors');
11
11
  const {normalizeReceipts, phpTruthy, findRow} = require('../support/receipts');
12
12
  const {withLock, LockTimeoutError} = require('../support/lock');
13
+ const {deliveryLine} = require('../support/delivery');
13
14
 
14
15
  const TYPE_TIMER = OrderProduct.TYPE_TIMER;
15
16
  const DINE_IN = 'dine_in';
@@ -629,7 +630,7 @@ class PrintService {
629
630
  table: table ? table.label : null,
630
631
  courier: '',
631
632
  comment: order.comment ?? null,
632
- delivery_address: '',
633
+ delivery_address: deliveryLine(order.data?.delivery),
633
634
  print_name: receipts.print_name_of === 'team'
634
635
  ? (team ? translated(team.name, this.locale()) : null)
635
636
  : (context.getCompany() ? context.getCompany().name : null),
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Адрес доставки чека (orders.data.delivery): {address, phone, name}, строки обрезаны до 255
3
+ * символов. Без адреса — null. Зеркало Laravel OrderService::normalizeDelivery.
4
+ */
5
+ const FIELDS = ['address', 'phone', 'name'];
6
+
7
+ const clean = (value) => {
8
+ if (value === null || value === undefined || typeof value === 'object') {
9
+ return null;
10
+ }
11
+ const text = Array.from(String(value).trim()).slice(0, 255).join('');
12
+ return text === '' ? null : text;
13
+ };
14
+
15
+ const normalizeDelivery = (delivery) => {
16
+ if (!delivery || typeof delivery !== 'object' || Array.isArray(delivery)) {
17
+ return null;
18
+ }
19
+ const result = {};
20
+ for (const field of FIELDS) {
21
+ result[field] = clean(delivery[field]);
22
+ }
23
+ return result.address ? result : null;
24
+ };
25
+
26
+ /** Строка «Çatdırılma ünvanı» счёта: адрес · телефон · имя (Laravel OrderService::deliveryLine). */
27
+ const deliveryLine = (delivery) => {
28
+ if (!delivery || typeof delivery !== 'object' || !delivery.address) {
29
+ return '';
30
+ }
31
+ return [delivery.address, delivery.phone, delivery.name]
32
+ .filter((value) => value !== null && value !== undefined && value !== '')
33
+ .join(' · ');
34
+ };
35
+
36
+ module.exports = {normalizeDelivery, deliveryLine};
@@ -0,0 +1,124 @@
1
+ const {round2} = require('./num');
2
+
3
+ /**
4
+ * Цены продажи по филиалу и по источнику заказа (таблица product_prices) — порт
5
+ * App\Services\Pricing\PriceBook. Правило в двух местах — правь оба.
6
+ *
7
+ * Строка — переопределение цены товара или модификатора либо для филиала, либо для источника.
8
+ * Порядок: цена источника → цена филиала → базовая цена. Источник важнее: «Çatdırılma 13»
9
+ * действует во всех филиалах.
10
+ */
11
+ class PriceBook {
12
+ /**
13
+ * @param {object[]} rows строки product_prices
14
+ * @param {?string} companyId филиал кассы / чека
15
+ */
16
+ constructor(rows, companyId = null) {
17
+ this.companyId = companyId ? String(companyId) : null;
18
+ /** @type {Map<string, {company: Map<string, number>, source: Map<string, number>}>} */
19
+ this.items = new Map();
20
+ for (const row of rows || []) {
21
+ const key = row.product_id || row.modifier_set_modifier_id;
22
+ if (!key) {
23
+ continue;
24
+ }
25
+ if (!this.items.has(String(key))) {
26
+ this.items.set(String(key), {company: new Map(), source: new Map()});
27
+ }
28
+ const item = this.items.get(String(key));
29
+ if (row.order_source_id) {
30
+ item.source.set(String(row.order_source_id), round2(Number(row.price)));
31
+ } else if (row.company_id) {
32
+ item.company.set(String(row.company_id), round2(Number(row.price)));
33
+ }
34
+ }
35
+ }
36
+
37
+ /** Цена в филиале без учёта источника — её касса показывает по умолчанию. */
38
+ branch(id, base) {
39
+ const item = this.items.get(String(id));
40
+ const company = item && this.companyId ? item.company.get(this.companyId) : undefined;
41
+ return company !== undefined ? company : round2(Number(base) || 0);
42
+ }
43
+
44
+ /** Цена позиции в чеке: источник → филиал → базовая. */
45
+ resolve(id, base, orderSourceId) {
46
+ const item = this.items.get(String(id));
47
+ if (item && orderSourceId && item.source.has(String(orderSourceId))) {
48
+ return item.source.get(String(orderSourceId));
49
+ }
50
+ return this.branch(id, base);
51
+ }
52
+
53
+ /** Есть ли у позиции своя цена для источника или этого филиала. */
54
+ overrides(id, orderSourceId) {
55
+ const item = this.items.get(String(id));
56
+ if (!item) {
57
+ return false;
58
+ }
59
+ return Boolean((orderSourceId && item.source.has(String(orderSourceId)))
60
+ || (this.companyId && item.company.has(this.companyId)));
61
+ }
62
+
63
+ /** { order_source_id: цена } или null — касса показывает source_prices[источник] ?? price. */
64
+ sourcePrices(id) {
65
+ const item = this.items.get(String(id));
66
+ if (!item || !item.source.size) {
67
+ return null;
68
+ }
69
+ return Object.fromEntries(item.source);
70
+ }
71
+ }
72
+
73
+ // Postgres облака: таблицу создаёт миграция Laravel. Пока её нет — цены базовые, а не 500.
74
+ // Проверка без ошибки запроса: упавший SELECT в транзакции Postgres ломает всю транзакцию.
75
+ // Найденная таблица запоминается навсегда, отсутствие — на минуту.
76
+ let presentAt = null;
77
+ let missingAt = 0;
78
+ const hasTable = async (db) => {
79
+ if (db.dialect !== 'pgsql' || presentAt) {
80
+ return true;
81
+ }
82
+ if (Date.now() - missingAt < 60_000) {
83
+ return false;
84
+ }
85
+ const row = await db.get("SELECT to_regclass('product_prices') AS name", []);
86
+ if (row && row.name) {
87
+ presentAt = Date.now();
88
+ return true;
89
+ }
90
+ missingAt = Date.now();
91
+ return false;
92
+ };
93
+
94
+ const safeAll = async (db, sql, params) => (await hasTable(db)) ? db.all(sql, params) : [];
95
+
96
+ /** Все цены команды — справочник кассы (app.init / products.get). */
97
+ const forTeam = async (db, teamId, companyId) => {
98
+ if (!teamId) {
99
+ return new PriceBook([], companyId);
100
+ }
101
+ return new PriceBook(await safeAll(db, 'SELECT * FROM product_prices WHERE team_id = ?', [teamId]), companyId);
102
+ };
103
+
104
+ /** Цены конкретных товаров/модификаторов — для добавления в чек. */
105
+ const forItems = async (db, {productIds = [], modifierIds = []}, companyId) => {
106
+ const products = productIds.filter(Boolean).map(String);
107
+ const modifiers = modifierIds.filter(Boolean).map(String);
108
+ if (!products.length && !modifiers.length) {
109
+ return new PriceBook([], companyId);
110
+ }
111
+ const where = [];
112
+ const params = [];
113
+ if (products.length) {
114
+ where.push(`product_id IN (${products.map(() => '?').join(', ')})`);
115
+ params.push(...products);
116
+ }
117
+ if (modifiers.length) {
118
+ where.push(`modifier_set_modifier_id IN (${modifiers.map(() => '?').join(', ')})`);
119
+ params.push(...modifiers);
120
+ }
121
+ return new PriceBook(await safeAll(db, `SELECT * FROM product_prices WHERE ${where.join(' OR ')}`, params), companyId);
122
+ };
123
+
124
+ module.exports = {PriceBook, forTeam, forItems};
@@ -127,6 +127,15 @@ const render = (report) => {
127
127
  }
128
128
  lines.push('');
129
129
 
130
+ // Источники заказа (в зале / с собой / доставка / агрегаторы) — то же в telegram/report.blade.php.
131
+ const sources = rowsOf(data.order_sources);
132
+ if (sources.length) {
133
+ lines.push(RULE, `🧾 <b>${e('Sifariş mənbələri')}</b>`, '<pre>');
134
+ lines.push(...sources.map(labelValue));
135
+ lines.push('</pre>');
136
+ }
137
+ lines.push('');
138
+
130
139
  const footer = report.footer;
131
140
  if (footer && typeof footer === 'object' && Object.keys(footer).length) {
132
141
  lines.push(RULE, `💰 <b>${e('YEKUN')}</b>`);
package/app/db/driver.js CHANGED
@@ -179,6 +179,12 @@ const openPostgres = () => {
179
179
  driverName = 'pg';
180
180
 
181
181
  const pool = new pg.Pool(pgConfig());
182
+ // Простаивающее соединение оборвалось (рестарт Postgres, сеть): pg шлёт 'error' на пул, и без
183
+ // слушателя это необработанное исключение — процесс облачного Node падал. Пул сам выбросит
184
+ // мёртвое соединение и откроет новое на следующем запросе.
185
+ pool.on('error', (e) => {
186
+ console.error('[db] idle pg client error:', e && e.message);
187
+ });
182
188
 
183
189
  /** Обёртка над клиентом/пулом — одинаковая, чтобы транзакция давала тот же интерфейс. */
184
190
  const wrap = (executor, isTransaction = false) => ({
@@ -0,0 +1,17 @@
1
+ -- Цены продажи по филиалу и по источнику заказа (Laravel 2026_11_13_000001, support/prices.js).
2
+ -- Строка — цена товара (product_id) или модификатора (modifier_set_modifier_id) для филиала
3
+ -- (company_id) либо для источника (order_source_id). Заполняет снапшот.
4
+ CREATE TABLE IF NOT EXISTS product_prices (
5
+ id TEXT PRIMARY KEY,
6
+ team_id TEXT,
7
+ product_id TEXT,
8
+ modifier_set_modifier_id TEXT,
9
+ company_id TEXT,
10
+ order_source_id TEXT,
11
+ price REAL,
12
+ created_at TEXT,
13
+ updated_at TEXT,
14
+ synced_at TEXT
15
+ );
16
+ CREATE INDEX IF NOT EXISTS product_prices_product ON product_prices (product_id);
17
+ CREATE INDEX IF NOT EXISTS product_prices_modifier ON product_prices (modifier_set_modifier_id);
@@ -59,14 +59,24 @@ const verify = async (auth) => {
59
59
  validateStatus: () => true,
60
60
  });
61
61
  } catch (e) {
62
- return {ok: false, reason: 'verify_unavailable'};
62
+ return {ok: false, reason: 'verify_unavailable', transient: true};
63
63
  }
64
64
  if (response.status !== 200 || !response.data || response.data.token_matches !== true) {
65
- return {ok: false, reason: `verify_failed_${response.status}`};
65
+ // Laravel недоступен / в деплое / перегружен / лицензия — пройдёт само; 401/403 и
66
+ // чужой токен — нет (касса переподключится с новой сессией сама).
67
+ const transient = response.status >= 500 || [402, 404, 408, 429].includes(response.status);
68
+ return {ok: false, reason: `verify_failed_${response.status}`, transient};
66
69
  }
67
70
  return {ok: true};
68
71
  };
69
72
 
73
+ // MASTER_RELAY_VERIFY_RETRY (по умолчанию выкл. = как раньше): разовый сбой проверки больше не
74
+ // оставляет подключённый сокет мастера вне реестра до его следующего обрыва — проверка
75
+ // повторяется с паузой, пока сокет жив. Без этого после рестарта облака или деплоя Laravel
76
+ // все клиенты заведения через облако получали master_offline часами.
77
+ const VERIFY_RETRY_DELAYS_MS = [3000, 10000, 30000, 60000];
78
+ const verifyRetryEnabled = () => (process.env.MASTER_RELAY_VERIFY_RETRY ?? 'false') === 'true';
79
+
70
80
  /**
71
81
  * Сигнальный сокет с handshake.auth.master. company_id в query сокета обязан совпасть с
72
82
  * тем, что проверено токеном, иначе касса слушала бы комнату чужой компании.
@@ -78,9 +88,18 @@ const register = (socket) => {
78
88
  socket.on('disconnect', () => unregister(socket));
79
89
 
80
90
  (async () => {
81
- const result = String(auth.company_id || '') === queryCompanyId
91
+ let result = String(auth.company_id || '') === queryCompanyId
82
92
  ? await verify(auth)
83
93
  : {ok: false, reason: 'company_mismatch'};
94
+ for (let attempt = 0; !result.ok && result.transient && verifyRetryEnabled() && socket.connected; attempt++) {
95
+ const delay = VERIFY_RETRY_DELAYS_MS[Math.min(attempt, VERIFY_RETRY_DELAYS_MS.length - 1)];
96
+ console.warn(`[relay] master verify failed (${result.reason}) terminal=${auth.terminal_id}, retry in ${delay / 1000}s`);
97
+ await new Promise((resolve) => setTimeout(resolve, delay));
98
+ if (!socket.connected) {
99
+ return;
100
+ }
101
+ result = await verify(auth);
102
+ }
84
103
  if (!socket.connected) {
85
104
  return;
86
105
  }
package/app/sync/index.js CHANGED
@@ -580,6 +580,8 @@ const applyCompanyMode = async (data) => {
580
580
  await state.store.block((s) => kv.set(s, 'company_mode', state.company));
581
581
  };
582
582
 
583
+ const MODE_NOT_FOUND_TO_ONLINE = 3;
584
+
583
585
  const pollMode = async () => {
584
586
  state.lastModePoll = Date.now();
585
587
  state.modeRequested = false;
@@ -589,12 +591,18 @@ const pollMode = async () => {
589
591
  } catch (e) {
590
592
  state.lastModeError = e.message;
591
593
  if (e.name === 'CloudRequestError' && e.status === 404) {
592
- // Старый Laravel или Sync API выключен: модели M нет — заведение в онлайне.
593
- await applyCompanyMode({state: 'online', master_terminal_id: null, epoch: 0, enabled: false});
594
+ // Старый Laravel или Sync API выключен: модели M нет — заведение в онлайне. Но 404 отдаёт
595
+ // и Traefik, пока контейнер облака пересоздаётся при деплое: разовый 404 снимал мастера
596
+ // на ~10 с (фронт уходил в облако, relay — not_master). Решаем по нескольким подряд.
597
+ state.modeNotFound = (state.modeNotFound || 0) + 1;
598
+ if (state.modeNotFound >= MODE_NOT_FOUND_TO_ONLINE) {
599
+ await applyCompanyMode({state: 'online', master_terminal_id: null, epoch: 0, enabled: false});
600
+ }
594
601
  return;
595
602
  }
596
603
  throw e;
597
604
  }
605
+ state.modeNotFound = 0;
598
606
  state.lastModeError = null;
599
607
  await applyCompanyMode(response);
600
608
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@alizadefaig/pos-server",
3
- "version": "1.2.33",
3
+ "version": "1.2.35",
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": {
@@ -35,6 +35,7 @@ registerOfflineRoutes(router, '/api', {
35
35
  servingId: get(p, 'serving_id'),
36
36
  id: get(p, 'id'),
37
37
  slotIndex: get(p, 'slot_index', 1),
38
+ delivery: get(p, 'delivery'),
38
39
  })],
39
40
 
40
41
  'orders.addProduct': [MUTATION, ({orders}, p) => orders.addProduct({
package/server.js CHANGED
@@ -182,6 +182,18 @@ const runExpress = (conf = null) => {
182
182
  // ==== Start Listen ====
183
183
  const hostname = appConfig.host || 'localhost';
184
184
  netstats.attach(server, {host: hostname, port});
185
+ // Порт занят (прежний процесс ещё не отпустил его после аварийного выхода, вторая копия) —
186
+ // раньше необработанное исключение: на кассе диалог Electron, локального Node нет до ручного
187
+ // перезапуска. Теперь повторяем, пока порт не освободится.
188
+ const LISTEN_RETRY_MS = 3000;
189
+ server.on('error', (e) => {
190
+ if (e && (e.code === 'EADDRINUSE' || e.code === 'EACCES') && !server.listening) {
191
+ console.error(`[server] port ${port} unavailable (${e.code}), retry in ${LISTEN_RETRY_MS / 1000}s`);
192
+ setTimeout(() => server.listen(port, hostname), LISTEN_RETRY_MS);
193
+ return;
194
+ }
195
+ console.error('[server] error:', e && e.message);
196
+ });
185
197
  server.listen(port, hostname, async () => {
186
198
  if (hostname === 'localhost') {
187
199
  console.log(`Server started on ${hostname} http://${hostname}:${port}`);