@alizadefaig/pos-server 1.2.32 → 1.2.34

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';
@@ -366,6 +367,7 @@ class PrintService {
366
367
  const printerTerminal = await this.printers.findTerminal(printer.terminal_id);
367
368
  jobs.push({
368
369
  workshopId,
370
+ printerId: printer.id,
369
371
  printData: {
370
372
  data: printData.data,
371
373
  terminal_id: printer.terminal_id,
@@ -376,15 +378,28 @@ class PrintService {
376
378
  }
377
379
  }
378
380
 
381
+ // Два принтера цеха на одном терминале с одним именем («ESC/POS») дали бы один ключ —
382
+ // терминал принял бы второй бегунок за повтор первого и не напечатал. Таким ключ
383
+ // различаем id принтера; с уникальными именами ключ прежний (как в Laravel).
384
+ const nameKeyOf = (workshopId, printData) => `${workshopId}|${printData.terminal_id}|${str(printData.printer.name)}`;
385
+ const nameCounts = new Map();
386
+ for (const {workshopId, printData} of jobs) {
387
+ const nameKey = nameKeyOf(workshopId, printData);
388
+ nameCounts.set(nameKey, (nameCounts.get(nameKey) || 0) + 1);
389
+ }
390
+
379
391
  const receipts = jobs.length ? await this.receiptsOf(team) : null;
380
- const requests = jobs.map(({workshopId, printData}) => {
392
+ const requests = jobs.map(({workshopId, printerId, printData}) => {
381
393
  // Стабильный per-ticket ключ: одинаков при повторе того же состояния — терминал
382
394
  // распознаёт дубль и не печатает второй раз. request_id в ключ не входит.
383
395
  const ticketSig = (syncOrderProducts.get(workshopId) || [])
384
396
  .map((opId) => `${opId}:${qtyById.has(opId) ? qtyById.get(opId) : 0}`)
385
397
  .sort()
386
398
  .join('|');
387
- const printIdempotencyKey = `${id}:${workshopId}:${printData.terminal_id}:${str(printData.printer.name)}:${md5(ticketSig)}`;
399
+ const printerKeyPart = nameCounts.get(nameKeyOf(workshopId, printData)) > 1
400
+ ? `${str(printData.printer.name)}#${printerId}`
401
+ : str(printData.printer.name);
402
+ const printIdempotencyKey = `${id}:${workshopId}:${printData.terminal_id}:${printerKeyPart}:${md5(ticketSig)}`;
388
403
 
389
404
  return {
390
405
  workshopId,
@@ -615,7 +630,7 @@ class PrintService {
615
630
  table: table ? table.label : null,
616
631
  courier: '',
617
632
  comment: order.comment ?? null,
618
- delivery_address: '',
633
+ delivery_address: deliveryLine(order.data?.delivery),
619
634
  print_name: receipts.print_name_of === 'team'
620
635
  ? (team ? translated(team.name, this.locale()) : null)
621
636
  : (context.getCompany() ? context.getCompany().name : null),
@@ -202,7 +202,7 @@ class StorageService {
202
202
  }
203
203
 
204
204
  /** Позиция, её товар, склад и единицы; null — склада нет, двигать некуда. */
205
- async prepare(orderProductOrId) {
205
+ async prepare(orderProductOrId, sign = -1) {
206
206
  const orderProductId = typeof orderProductOrId === 'object' && orderProductOrId
207
207
  ? orderProductOrId.id
208
208
  : orderProductOrId;
@@ -217,14 +217,15 @@ class StorageService {
217
217
  throw new Error('позиция не найдена');
218
218
  }
219
219
  const product = await this.db.get(
220
- 'SELECT id, type, unit_id FROM products WHERE id = ?',
220
+ `SELECT id, type, unit_id, product_workshop_id, product_category_id, ignore_workshop
221
+ FROM products WHERE id = ?`,
221
222
  [orderProduct.product_id]
222
223
  );
223
224
  if (!product) {
224
225
  throw new Error('товар не найден');
225
226
  }
226
227
 
227
- const storageId = await this.storageId();
228
+ const storageId = await this.storageId(product, orderProduct.id, sign);
228
229
  if (!storageId) {
229
230
  return null;
230
231
  }
@@ -234,7 +235,7 @@ class StorageService {
234
235
 
235
236
  /** Только модификаторы из списка — тех. карту блюда и сам товар не трогает. */
236
237
  async applyModifiers(orderProductOrId, modifiers, quantity, type, sign) {
237
- const target = await this.prepare(orderProductOrId);
238
+ const target = await this.prepare(orderProductOrId, sign);
238
239
  if (!target || target.product.type !== TYPE_DISH) {
239
240
  return;
240
241
  }
@@ -242,7 +243,7 @@ class StorageService {
242
243
  }
243
244
 
244
245
  async apply(orderProductOrId, quantity, type, sign) {
245
- const target = await this.prepare(orderProductOrId);
246
+ const target = await this.prepare(orderProductOrId, sign);
246
247
  if (!target) {
247
248
  return;
248
249
  }
@@ -333,10 +334,31 @@ class StorageService {
333
334
  return unit;
334
335
  }
335
336
 
336
- /** Склад заведения (`company_storage`), иначе первый склад команды. */
337
- async storageId() {
337
+ /**
338
+ * Склад списания позиции — порт Platform\StorageService::storageId.
339
+ * Возврат ($sign > 0) — на склад, с которого позицию списали. Продажа — по правилу заведения
340
+ * `companies.storage_withdraw_type` (склад цеха; by_terminal_and_workshop — пары «терминал +
341
+ * цех», нет пары — любое правило цеха), иначе склад заведения, иначе первый склад команды.
342
+ */
343
+ async storageId(product = null, orderProductId = null, sign = -1) {
338
344
  const {teamId, companyId} = this.context;
345
+ if (sign > 0 && orderProductId) {
346
+ const sold = await this.db.value(
347
+ `SELECT storage_id FROM storage_stock_transactions
348
+ WHERE external_id = ? AND type = 'sale'
349
+ ORDER BY datetime DESC LIMIT 1`,
350
+ [orderProductId]
351
+ );
352
+ if (sold) {
353
+ return sold;
354
+ }
355
+ }
339
356
  if (companyId) {
357
+ // Схема без правил (старая локальная БД) — как раньше, склад заведения.
358
+ const byRule = await this.ruleStorageId(companyId, product).catch(() => null);
359
+ if (byRule) {
360
+ return byRule;
361
+ }
340
362
  const storageId = await this.db.value(
341
363
  'SELECT storage_id FROM company_storage WHERE company_id = ? LIMIT 1',
342
364
  [companyId]
@@ -354,6 +376,49 @@ class StorageService {
354
376
  );
355
377
  }
356
378
 
379
+ /** Склад по правилу «склад цеха»; null — правила нет. */
380
+ async ruleStorageId(companyId, product) {
381
+ const workshopId = await this.workshopId(product);
382
+ if (!workshopId) {
383
+ return null;
384
+ }
385
+ const type = await this.db.value('SELECT storage_withdraw_type FROM companies WHERE id = ?', [companyId]);
386
+ if (type !== 'by_workshop' && type !== 'by_terminal_and_workshop') {
387
+ return null;
388
+ }
389
+ const base = `SELECT r.storage_id FROM company_storage_withdraw_rules r
390
+ JOIN storages s ON s.id = r.storage_id AND s.deleted_at IS NULL
391
+ WHERE r.deleted_at IS NULL AND r.company_id = ? AND r.workshop_id = ?`;
392
+ const terminalId = this.context.terminalId;
393
+ if (type === 'by_terminal_and_workshop' && terminalId) {
394
+ const own = await this.db.value(
395
+ `${base} AND r.terminal_id = ? ORDER BY r.created_at, r.id LIMIT 1`,
396
+ [companyId, workshopId, terminalId]
397
+ );
398
+ if (own) {
399
+ return own;
400
+ }
401
+ }
402
+ return await this.db.value(`${base} ORDER BY r.created_at, r.id LIMIT 1`, [companyId, workshopId]);
403
+ }
404
+
405
+ /** Мутатор Product::productWorkshopId: ignore_workshop гасит цех, пустой — у категории. */
406
+ async workshopId(product) {
407
+ if (!product || toBool(product.ignore_workshop)) {
408
+ return null;
409
+ }
410
+ if (product.product_workshop_id) {
411
+ return product.product_workshop_id;
412
+ }
413
+ if (!product.product_category_id) {
414
+ return null;
415
+ }
416
+ return await this.db.value(
417
+ 'SELECT product_workshop_id FROM product_categories WHERE id = ?',
418
+ [product.product_category_id]
419
+ ) || null;
420
+ }
421
+
357
422
  /** StockService::register без суммы (продажа себестоимость не меняет) + applyToStock. */
358
423
  async move(orderProduct, product, type, quantity, storageId, units) {
359
424
  // Нулевое движение не пишется (как в PHP — до проверки единицы).
@@ -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>`);
@@ -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);
package/package.json CHANGED
@@ -1,45 +1,45 @@
1
1
  {
2
- "name": "@alizadefaig/pos-server",
3
- "version": "1.2.32",
4
- "description": "Server in express js with Proxy to Main server if need and will work with Electron JS",
5
- "homepage": "https://gitlab.com/faigalizade/pos-server#readme",
6
- "bugs": {
7
- "url": "https://gitlab.com/faigalizade/pos-server/issues"
8
- },
9
- "repository": {
10
- "type": "git",
11
- "url": "git+ssh://git@gitlab.com/faigalizade/pos-server.git"
12
- },
13
- "dependencies": {
14
- "@alizadefaig/cafetap-escpos-usb": "^1.0.1",
15
- "axios": "^1.13.2",
16
- "bcryptjs": "^3.0.3",
17
- "cors": "^2.8.5",
18
- "escpos": "^3.0.0-alpha.6",
19
- "escpos-network": "^3.0.0-alpha.5",
20
- "express": "^5.1.0",
21
- "http-proxy-middleware": "^3.0.5",
22
- "lodash": "^4.17.21",
23
- "moment": "^2.30.1",
24
- "multer": "^2.0.2",
25
- "pg": "^8.22.0",
26
- "redis": "^5.10.0",
27
- "sequelize": "^6.37.7",
28
- "sharp": "^0.34.5",
29
- "socket.io": "^4.8.1",
30
- "socket.io-client": "^4.8.1",
31
- "uuid": "^13.0.0"
32
- },
33
- "license": "ISC",
34
- "author": "faigalizade@newave.az",
35
- "type": "commonjs",
36
- "main": "server.js",
37
- "directories": {
38
- "lib": "lib"
39
- },
40
- "scripts": {
41
- "test": "node --test --test-concurrency=1 \"test/**/*.test.js\"",
42
- "test:watch": "node --test --watch --test-concurrency=1 \"test/**/*.test.js\"",
43
- "start": "node server.js"
44
- }
2
+ "name": "@alizadefaig/pos-server",
3
+ "version": "1.2.34",
4
+ "description": "Server in express js with Proxy to Main server if need and will work with Electron JS",
5
+ "homepage": "https://gitlab.com/faigalizade/pos-server#readme",
6
+ "bugs": {
7
+ "url": "https://gitlab.com/faigalizade/pos-server/issues"
8
+ },
9
+ "repository": {
10
+ "type": "git",
11
+ "url": "git+ssh://git@gitlab.com/faigalizade/pos-server.git"
12
+ },
13
+ "dependencies": {
14
+ "@alizadefaig/cafetap-escpos-usb": "^1.0.1",
15
+ "axios": "^1.13.2",
16
+ "bcryptjs": "^3.0.3",
17
+ "cors": "^2.8.5",
18
+ "escpos": "^3.0.0-alpha.6",
19
+ "escpos-network": "^3.0.0-alpha.5",
20
+ "express": "^5.1.0",
21
+ "http-proxy-middleware": "^3.0.5",
22
+ "lodash": "^4.17.21",
23
+ "moment": "^2.30.1",
24
+ "multer": "^2.0.2",
25
+ "pg": "^8.22.0",
26
+ "redis": "^5.10.0",
27
+ "sequelize": "^6.37.7",
28
+ "sharp": "^0.34.5",
29
+ "socket.io": "^4.8.1",
30
+ "socket.io-client": "^4.8.1",
31
+ "uuid": "^13.0.0"
32
+ },
33
+ "license": "ISC",
34
+ "author": "faigalizade@newave.az",
35
+ "type": "commonjs",
36
+ "main": "server.js",
37
+ "directories": {
38
+ "lib": "lib"
39
+ },
40
+ "scripts": {
41
+ "test": "node --test --test-concurrency=1 \"test/**/*.test.js\"",
42
+ "test:watch": "node --test --watch --test-concurrency=1 \"test/**/*.test.js\"",
43
+ "start": "node server.js"
44
+ }
45
45
  }
@@ -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({