@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.
Files changed (117) hide show
  1. package/app/core/Context.js +170 -0
  2. package/app/core/auth/index.js +133 -0
  3. package/app/core/auth/license.js +45 -0
  4. package/app/core/auth/posMode.js +260 -0
  5. package/app/core/auth/sanctum.js +209 -0
  6. package/app/core/auth/scope.js +231 -0
  7. package/app/core/emitter.js +67 -0
  8. package/app/core/errors.js +18 -0
  9. package/app/core/guard.js +54 -0
  10. package/app/core/idempotency.js +196 -0
  11. package/app/core/index.js +374 -0
  12. package/app/core/models/Order.js +176 -0
  13. package/app/core/models/OrderProduct.js +249 -0
  14. package/app/core/repositories/EventRepository.js +51 -0
  15. package/app/core/repositories/OrderProductRepository.js +225 -0
  16. package/app/core/repositories/OrderRepository.js +274 -0
  17. package/app/core/repositories/OutboxRepository.js +52 -0
  18. package/app/core/repositories/PaymentRepository.js +43 -0
  19. package/app/core/repositories/PrinterRepository.js +179 -0
  20. package/app/core/repositories/ProductRelations.js +148 -0
  21. package/app/core/repositories/SnapshotRepository.js +197 -0
  22. package/app/core/resources/HallResource.js +54 -0
  23. package/app/core/resources/OrderResource.js +177 -0
  24. package/app/core/resources/ProductResource.js +186 -0
  25. package/app/core/resources/ShiftResource.js +42 -0
  26. package/app/core/resources/TeamResource.js +117 -0
  27. package/app/core/resources/casts.js +55 -0
  28. package/app/core/services/AuthService.js +468 -0
  29. package/app/core/services/InitService.js +279 -0
  30. package/app/core/services/KdsService.js +639 -0
  31. package/app/core/services/OrderProductService.js +309 -0
  32. package/app/core/services/OrderService.js +911 -0
  33. package/app/core/services/PrintService.js +715 -0
  34. package/app/core/services/PrinterAdminService.js +324 -0
  35. package/app/core/services/ReportService.js +581 -0
  36. package/app/core/services/ShiftService.js +199 -0
  37. package/app/core/services/StorageService.js +425 -0
  38. package/app/core/services/TelegramService.js +162 -0
  39. package/app/core/support/db.js +48 -0
  40. package/app/core/support/ids.js +9 -0
  41. package/app/core/support/imageData.js +135 -0
  42. package/app/core/support/images.js +92 -0
  43. package/app/core/support/lanHandoff.js +105 -0
  44. package/app/core/support/lang.js +98 -0
  45. package/app/core/support/lock.js +63 -0
  46. package/app/core/support/num.js +51 -0
  47. package/app/core/support/php.js +125 -0
  48. package/app/core/support/pin.js +34 -0
  49. package/app/core/support/receipts.js +278 -0
  50. package/app/core/support/rows.js +38 -0
  51. package/app/core/support/str.js +31 -0
  52. package/app/core/support/teamSettings.js +77 -0
  53. package/app/core/support/telegram.js +253 -0
  54. package/app/core/support/telegramReport.js +143 -0
  55. package/app/core/support/time.js +58 -0
  56. package/app/core/support/tz.js +80 -0
  57. package/app/core/support/unlock.js +113 -0
  58. package/app/core/timers.js +151 -0
  59. package/app/db/Database.js +197 -1
  60. package/app/db/Migrator.js +45 -0
  61. package/app/db/driver.js +248 -0
  62. package/app/db/migrations/001_snapshot.sql +214 -0
  63. package/app/db/migrations/002_orders.sql +190 -0
  64. package/app/db/migrations/003_idempotency.sql +18 -0
  65. package/app/db/migrations/004_shifts.sql +21 -0
  66. package/app/db/migrations/005_printers.sql +43 -0
  67. package/app/db/migrations/006_sync.sql +136 -0
  68. package/app/db/migrations/007_printer_mac.sql +4 -0
  69. package/app/db/migrations/008_kds.sql +94 -0
  70. package/app/db/migrations/009_product_categories.sql +20 -0
  71. package/app/db/migrations/010_printers_sync.sql +43 -0
  72. package/app/db/migrations/011_team_logos.sql +13 -0
  73. package/app/db/migrations/012_category_workshop.sql +4 -0
  74. package/app/monitor.js +185 -0
  75. package/app/printers/address.js +348 -0
  76. package/app/proxy.js +11 -3
  77. package/app/relay/masters.js +173 -0
  78. package/app/security.js +218 -0
  79. package/app/socket/auth.js +160 -0
  80. package/app/socket/request.js +87 -0
  81. package/app/socket/server.js +121 -19
  82. package/app/sync/bridge.js +162 -0
  83. package/app/sync/cloud.js +120 -0
  84. package/app/sync/config.js +185 -0
  85. package/app/sync/discovery.js +546 -0
  86. package/app/sync/index.js +1644 -0
  87. package/app/sync/kv.js +26 -0
  88. package/app/sync/lan.js +139 -0
  89. package/app/sync/lanDevices.js +177 -0
  90. package/app/sync/lanFront.js +431 -0
  91. package/app/sync/mirror.js +236 -0
  92. package/app/sync/push.js +324 -0
  93. package/app/sync/reconcile.js +161 -0
  94. package/app/sync/signal.js +202 -0
  95. package/app/sync/snapshot.js +189 -0
  96. package/app/sync/store.js +234 -0
  97. package/app/tcp.js +11 -2
  98. package/package.json +6 -3
  99. package/routes/common/api/auth.js +38 -0
  100. package/routes/common/api/kds.js +37 -0
  101. package/routes/common/api/orderProducts.js +81 -0
  102. package/routes/common/api/orders.js +176 -0
  103. package/routes/common/api/printersCore.js +35 -0
  104. package/routes/common/api/reports.js +29 -0
  105. package/routes/common/socket.js +15 -35
  106. package/routes/common/tax.js +29 -7
  107. package/routes/desktop/api/offline.js +146 -0
  108. package/routes/desktop/api/printers.js +288 -5
  109. package/routes/desktop/local.js +130 -0
  110. package/routes/server/api/login.js +37 -0
  111. package/routes/server/api/platform.js +31 -0
  112. package/routes/server/api/shifts.js +25 -0
  113. package/routes/server/master.js +24 -0
  114. package/server.js +72 -3
  115. package/utils.js +6 -0
  116. package/tests/usbDiagnose.js +0 -59
  117. package/tests/usbPrinterTest.js +0 -20
@@ -0,0 +1,309 @@
1
+ const OrderService = require('./OrderService');
2
+ const StorageService = require('./StorageService');
3
+ const OrderProduct = require('../models/OrderProduct');
4
+ const {notFound} = require('../errors');
5
+ const {money, qty, toFloat, toInt, toBool} = require('../support/num');
6
+ const {nowSql, diffInSeconds, now} = require('../support/time');
7
+ const {htmlspecialchars} = require('../support/str');
8
+
9
+ const TYPE_TIMER = OrderProduct.TYPE_TIMER;
10
+
11
+ /**
12
+ * Порт App\Services\Platform\OrderProductService — операции над позицией чека.
13
+ * Разделяет репозитории и контекст с OrderService: часть операций (append) уходит туда.
14
+ */
15
+ class OrderProductService {
16
+ constructor({db, context, emitter = null}) {
17
+ this.db = db;
18
+ this.context = context;
19
+ this.orderService = new OrderService({db, context, emitter});
20
+
21
+ this.orders = this.orderService.orders;
22
+ this.orderProducts = this.orderService.orderProducts;
23
+ this.snapshot = this.orderService.snapshot;
24
+ }
25
+
26
+ async findOrFail(orderProductId) {
27
+ const orderProduct = await this.orderProducts.find(orderProductId);
28
+ if (!orderProduct) {
29
+ throw notFound('Order product not found');
30
+ }
31
+ return orderProduct;
32
+ }
33
+
34
+ /** Позиция + guard её чека (`$orderProduct->order` в Laravel — без удалённых). */
35
+ async findMutable(orderProductId, action) {
36
+ const orderProduct = await this.findOrFail(orderProductId);
37
+ this.orderService.assertMutable(await this.orders.findRow(orderProduct.order_id), action);
38
+ return orderProduct;
39
+ }
40
+
41
+ // ------------------------------------------------------------ таймеры
42
+
43
+ async stopTimer({orderProductId, orderId}) {
44
+ await this.findMutable(orderProductId, 'orderProducts.stopTimer');
45
+ await this.orderProducts.update(orderProductId, {is_stopped: 1, stopped_at: nowSql()});
46
+ await this.orderService.emitPatch(orderId);
47
+ return true;
48
+ }
49
+
50
+ async fixTimer({orderProductId, orderId}) {
51
+ await this.findMutable(orderProductId, 'orderProducts.fixTimer');
52
+ await this.orderProducts.update(orderProductId, {is_stopped: 1, stopped_at: nowSql(), is_fixed: 1});
53
+ await this.orderService.emitPatch(orderId);
54
+ return true;
55
+ }
56
+
57
+ /** Пауза не должна оплачиваться: время простоя копится в paused_seconds. */
58
+ async resumeTimer({orderProductId, orderId}) {
59
+ const orderProduct = await this.findMutable(orderProductId, 'orderProducts.resumeTimer');
60
+ const additionalPaused = orderProduct.stopped_at
61
+ ? Math.max(0, diffInSeconds(orderProduct.stopped_at, now()))
62
+ : 0;
63
+ await this.orderProducts.update(orderProductId, {
64
+ is_stopped: 0,
65
+ stopped_at: null,
66
+ paused_seconds: toInt(orderProduct.paused_seconds, 0) + additionalPaused,
67
+ });
68
+ await this.orderService.emitPatch(orderId);
69
+ return true;
70
+ }
71
+
72
+ async setTimerLimit({orderProductId, orderId, timerLimitMinutes}) {
73
+ await this.findMutable(orderProductId, 'orderProducts.setTimerLimit');
74
+ await this.orderProducts.update(orderProductId, {
75
+ timer_limit_minutes: toFloat(timerLimitMinutes) > 0 ? toInt(timerLimitMinutes) : null,
76
+ });
77
+ await this.orderService.emitPatch(orderId);
78
+ return true;
79
+ }
80
+
81
+ // ---------------------------------------------------------- позиции
82
+
83
+ async transfer({orderProductId, orderId}) {
84
+ const orderProduct = await this.findOrFail(orderProductId);
85
+ const sourceOrderId = orderProduct.order_id;
86
+ // Перенос с/на закрытый чек — тот же перенос денег: проверяем оба.
87
+ this.orderService.assertMutable(await this.orders.findRow(sourceOrderId), 'orderProducts.transfer:source');
88
+ this.orderService.assertMutable(await this.orders.findRow(orderId), 'orderProducts.transfer:target');
89
+
90
+ await this.orderProducts.update(orderProductId, {order_id: orderId});
91
+
92
+ await this.orderService.saveEvents([{
93
+ event: 'products:transfer',
94
+ time: nowSql(),
95
+ order_id: orderId,
96
+ product_id: orderProductId,
97
+ }]);
98
+
99
+ await this.orderService.recalculate(sourceOrderId);
100
+ await this.orderService.recalculate(orderId);
101
+ await this.orderService.emitPatch(sourceOrderId);
102
+ await this.orderService.emitPatch(orderId);
103
+ return true;
104
+ }
105
+
106
+ async setQuantity({orderProductId, quantity}) {
107
+ const orderProduct = await this.findMutable(orderProductId, 'orderProducts.setQuantity');
108
+ quantity = qty(quantity);
109
+
110
+ if (quantity === orderProduct.quantity) {
111
+ return await this.orderProducts.findRow(orderProductId);
112
+ }
113
+
114
+ // Склад — по held(quantity, workshop_print_quantity) до и после правки. Снятое до печати
115
+ // возвращается сразу (rollback), снятое после печати держится списанным, пока отмена не
116
+ // уйдёт в цех (refund в OrderService.syncWorkshopCount).
117
+ const heldBefore = StorageService.held(orderProduct.quantity, orderProduct.workshop_print_quantity);
118
+
119
+ // Позиция, которую ещё не отправляли в цех, удаляется физически (мягко) —
120
+ // печатать по ней уже нечего.
121
+ if (quantity == 0 && orderProduct.workshop_print_quantity == 0) {
122
+ // До softDelete: движение блюда читает живые модификаторы позиции.
123
+ await this.orderService.storage.settle(orderProduct, heldBefore, StorageService.held(0, 0), 'rollback');
124
+ await this.orderProducts.softDelete(orderProductId);
125
+ await this.orderService.recalculate(orderProduct.order_id);
126
+ await this.orderService.emitPatch(orderProduct.order_id);
127
+ return true;
128
+ }
129
+
130
+ quantity = Math.max(quantity, 0);
131
+ await this.orderService.saveEvents([{
132
+ event: quantity > orderProduct.quantity ? 'orders:add_product' : 'orders:trash_product',
133
+ time: nowSql(),
134
+ order_id: orderProduct.order_id,
135
+ product_id: orderProduct.product_id,
136
+ quantity: qty(quantity - orderProduct.quantity),
137
+ }]);
138
+
139
+ const appendMode = toBool(this.context.setting('terminal_settings.append_order_product', false));
140
+ const hasTimerModifier = await this.snapshot.hasTimerModifier(orderProduct.product_id);
141
+
142
+ // Возврат ранее удалённой, но уже напечатанной позиции — просто правим количество,
143
+ // отдельной строкой такое добавлять нельзя (цех получил бы дубль).
144
+ const isRestoring = orderProduct.workshop_print_quantity > 0
145
+ && quantity > orderProduct.quantity
146
+ && quantity <= orderProduct.workshop_print_quantity;
147
+
148
+ const shouldAppend = !isRestoring
149
+ && quantity > orderProduct.quantity
150
+ && (appendMode || hasTimerModifier);
151
+
152
+ let isAppend = false;
153
+ let newProductId = null;
154
+
155
+ if (shouldAppend) {
156
+ // Прибавка уходит новой строкой (или доливается addProduct'ом в подходящую) — склад
157
+ // двигает addProduct. У этой строки количество не меняется, её held тоже.
158
+ const created = await this.orderService.addProduct({
159
+ productId: orderProduct.product_id,
160
+ orderId: orderProduct.order_id,
161
+ guestNumber: orderProduct.guest_number,
162
+ quantity: qty(quantity - orderProduct.quantity),
163
+ modifierSets: orderProduct.modifiers,
164
+ });
165
+ newProductId = created ? created.id : null;
166
+ isAppend = true;
167
+ } else {
168
+ const data = {quantity};
169
+ if (!isRestoring) {
170
+ data.modified_at = nowSql();
171
+ }
172
+ if (orderProduct.type === TYPE_TIMER) {
173
+ data.workshop_print_quantity = 0;
174
+ }
175
+ // Напечатанное — как до правки, как в PHP. Обнуление у повременной позиции ничего не
176
+ // меняет: склад она не двигает (тип товара не product и не dish).
177
+ await this.orderService.storage.settle(
178
+ orderProduct,
179
+ heldBefore,
180
+ StorageService.held(quantity, orderProduct.workshop_print_quantity),
181
+ 'rollback'
182
+ );
183
+ await this.orderProducts.update(orderProductId, data);
184
+
185
+ if (orderProduct.type === TYPE_TIMER && quantity == 0) {
186
+ await this.orderProducts.softDelete(orderProductId);
187
+ }
188
+ }
189
+
190
+ await this.orderService.recalculate(orderProduct.order_id);
191
+ await this.orderService.emitPatch(orderProduct.order_id);
192
+
193
+ return {
194
+ ...((await this.orderProducts.findRow(orderProductId)) || {}),
195
+ is_append: isAppend,
196
+ new_product_id: newProductId,
197
+ };
198
+ }
199
+
200
+ async setPrice({orderProductId, price}) {
201
+ const orderProduct = await this.findMutable(orderProductId, 'orderProducts.setPrice');
202
+ await this.orderProducts.update(orderProductId, {price: money(price)});
203
+ await this.orderService.recalculate(orderProduct.order_id);
204
+ await this.orderService.emitPatch(orderProduct.order_id);
205
+ return await this.orderProducts.findRow(orderProductId);
206
+ }
207
+
208
+ /** «Удаление» позиции — это количество 0: строка нужна печати, чтобы отбить сторно. */
209
+ async remove(orderProductId) {
210
+ const orderProduct = await this.findMutable(orderProductId, 'orderProducts.remove');
211
+ // Склад — как в setQuantity до 0: не напечатанное возвращается сразу, напечатанное
212
+ // держится списанным, пока отмена не уйдёт в цех.
213
+ await this.orderService.storage.settle(
214
+ orderProduct,
215
+ StorageService.held(orderProduct.quantity, orderProduct.workshop_print_quantity),
216
+ StorageService.held(0, orderProduct.workshop_print_quantity),
217
+ 'rollback'
218
+ );
219
+ await this.orderProducts.update(orderProductId, {quantity: 0});
220
+ await this.orderService.recalculate(orderProduct.order_id);
221
+ await this.orderService.emitPatch(orderProduct.order_id);
222
+ return true;
223
+ }
224
+
225
+ async setComment({orderProductId, comment}) {
226
+ const orderProduct = await this.findMutable(orderProductId, 'orderProducts.setComment');
227
+ const escaped = htmlspecialchars(comment);
228
+ if (orderProduct.comment === escaped) {
229
+ return await this.orderProducts.findRow(orderProductId);
230
+ }
231
+ await this.orderProducts.update(orderProductId, {comment: escaped});
232
+ await this.orderService.emitPatch(orderProduct.order_id);
233
+ return await this.orderProducts.findRow(orderProductId);
234
+ }
235
+
236
+ async updateModifiers({orderProductId, modifiers}) {
237
+ const orderProduct = await this.findMutable(orderProductId, 'orderProducts.updateModifiers');
238
+ const [sets, , hash] = await OrderService.collectModifierSets(this.snapshot, modifiers, this.context.locale());
239
+
240
+ await this.orderProducts.update(orderProductId, {modifications_sets_hash: hash});
241
+ await this.orderProducts.deleteModifierSets(orderProductId);
242
+ await this.orderProducts.insertModifierSets(orderProductId, sets);
243
+
244
+ // Склад: старые модификаторы возвращаются, новые списываются — на held позиции.
245
+ // orderProduct.modifiers прочитаны findMutable до удаления. Тот же набор — движений нет.
246
+ if ((hash || null) !== (orderProduct.modifications_sets_hash || null)) {
247
+ await this.orderService.storage.swapModifiers(
248
+ orderProduct,
249
+ orderProduct.modifiers,
250
+ StorageService.held(orderProduct.quantity, orderProduct.workshop_print_quantity)
251
+ );
252
+ }
253
+
254
+ await this.orderService.recalculate(orderProduct.order_id);
255
+ await this.orderService.emitPatch(orderProduct.order_id);
256
+ return true;
257
+ }
258
+
259
+ /**
260
+ * Перенос позиции другому гостю. Если у него уже есть такая же строка — сливаем,
261
+ * иначе в чеке появятся две одинаковые позиции у одного гостя.
262
+ */
263
+ async setGuestNumber({orderProductId, guestNumber}) {
264
+ const orderProduct = await this.findMutable(orderProductId, 'orderProducts.setGuestNumber');
265
+ const other = await this.orderProducts.findSameProductForGuest({
266
+ orderId: orderProduct.order_id,
267
+ productId: orderProduct.product_id,
268
+ guestNumber,
269
+ price: orderProduct.price,
270
+ hash: orderProduct.modifications_sets_hash,
271
+ excludeId: orderProduct.id,
272
+ });
273
+
274
+ if (!other) {
275
+ await this.orderProducts.update(orderProductId, {guest_number: toInt(guestNumber, 1)});
276
+ await this.orderService.emitPatch(orderProduct.order_id);
277
+ return await this.orderProducts.findRow(orderProductId);
278
+ }
279
+
280
+ const quantity = qty(other.quantity + orderProduct.quantity);
281
+ const printed = qty(other.workshop_print_quantity + orderProduct.workshop_print_quantity);
282
+ // Склад: held слитой строки не больше суммы held двух (снятое после печати у одной
283
+ // перекрывается количеством другой) — разница возвращается сразу.
284
+ const heldBefore = StorageService.held(other.quantity, other.workshop_print_quantity)
285
+ + StorageService.held(orderProduct.quantity, orderProduct.workshop_print_quantity);
286
+ const heldAfter = StorageService.held(quantity, printed);
287
+
288
+ await this.orderProducts.update(other.id, {
289
+ quantity,
290
+ total: money(other.total + orderProduct.total),
291
+ comment: other.comment || orderProduct.comment,
292
+ print_quantity: qty(other.print_quantity + orderProduct.print_quantity),
293
+ receipt_print_quantity: qty(other.receipt_print_quantity + orderProduct.receipt_print_quantity),
294
+ total_workshop_print_quantity: qty(other.total_workshop_print_quantity + orderProduct.total_workshop_print_quantity),
295
+ workshop_print_quantity: printed,
296
+ });
297
+ // Модификаторы у строк одинаковые (слияние только по тому же хэшу) — двигаем по B.
298
+ await this.orderService.storage.settle(other, heldBefore, heldAfter, 'rollback');
299
+ await this.orderProducts.softDelete(orderProductId);
300
+ await this.orderService.emitPatch(orderProduct.order_id);
301
+ return await this.orderProducts.findRow(other.id);
302
+ }
303
+
304
+ async getById(orderId) {
305
+ return await this.orderService.getById(orderId);
306
+ }
307
+ }
308
+
309
+ module.exports = OrderProductService;