@alizadefaig/pos-server 1.2.25 → 1.2.27
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 +85 -0
- package/app/core/resources/casts.js +55 -0
- package/app/core/services/AuthService.js +468 -0
- package/app/core/services/InitService.js +277 -0
- package/app/core/services/KdsService.js +639 -0
- package/app/core/services/OrderProductService.js +309 -0
- package/app/core/services/OrderService.js +903 -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 +20 -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 +35 -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 +111 -4
- package/utils.js +6 -0
- package/tests/usbPrinterTest.js +0 -20
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
const express = require('express');
|
|
2
|
+
const router = express.Router();
|
|
3
|
+
const {registerOfflineRoutes} = require('../../../app/core');
|
|
4
|
+
const scope = require('../../../app/core/auth/scope');
|
|
5
|
+
const {HttpError} = require('../../../app/core/errors');
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Порт AuthController::check — «этот токен действительно работает от имени этих заголовков?».
|
|
9
|
+
* Его спрашивает сокет облака (app/socket/auth.js) перед тем, как пустить кассу в комнату
|
|
10
|
+
* компании. Токен, команду и лицензию уже проверил облачный контур ядра (app/core/auth),
|
|
11
|
+
* здесь остаётся сверка принадлежности — всегда, при любом PLATFORM_SCOPE_CHECK.
|
|
12
|
+
*
|
|
13
|
+
* Только облако: на кассе токенов облака нет, событие уходит в прокси как раньше.
|
|
14
|
+
*/
|
|
15
|
+
registerOfflineRoutes(router, '/api', {
|
|
16
|
+
'auth.check': [{mutation: false, cloudOnly: true}, async (services, params, {event, ip, auth}) => {
|
|
17
|
+
if (!auth) {
|
|
18
|
+
// CORE_AUTH=false: проверять нечем — лучше отказ, чем «ok» без токена.
|
|
19
|
+
throw new HttpError(401, {message: 'Unauthenticated.'});
|
|
20
|
+
}
|
|
21
|
+
const reason = await scope.check(services.db, auth.auth, auth.platform, true);
|
|
22
|
+
if (reason !== null) {
|
|
23
|
+
scope.report(reason, {event, auth: auth.auth, platform: auth.platform, ip}, 'check');
|
|
24
|
+
const {status, body} = scope.checkFailed(reason);
|
|
25
|
+
throw new HttpError(status, body);
|
|
26
|
+
}
|
|
27
|
+
return {
|
|
28
|
+
ok: true,
|
|
29
|
+
user_id: auth.auth.userId,
|
|
30
|
+
team_id: scope.value(auth.platform.teamId),
|
|
31
|
+
company_id: scope.value(auth.platform.companyId),
|
|
32
|
+
terminal_id: scope.value(auth.platform.terminalId),
|
|
33
|
+
token_terminal_id: auth.auth.tokenTerminalId,
|
|
34
|
+
};
|
|
35
|
+
}],
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
module.exports = router;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
const express = require('express');
|
|
2
|
+
const router = express.Router();
|
|
3
|
+
const {registerOfflineRoutes} = require('../../../app/core');
|
|
4
|
+
const KdsService = require('../../../app/core/services/KdsService');
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Порт App\Http\Controllers\Api\Platform\KdsController — кухонный экран (KDS) и экран
|
|
8
|
+
* готовности. Тикеты создаёт отправка в цех (orders.sendToWorkshops), здесь — только их чтение
|
|
9
|
+
* и статусы с кухни. Ответы без обёртки, как у Laravel (withoutWrapping).
|
|
10
|
+
*
|
|
11
|
+
* При выключенном OFFLINE_CORE (или событии вне CORE_EVENTS) уходит в next() → Laravel.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
const get = (params, name, fallback = null) => {
|
|
15
|
+
const value = params[name];
|
|
16
|
+
return value === undefined || value === null ? fallback : value;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const MUTATION = {mutation: true};
|
|
20
|
+
const READ = {mutation: false};
|
|
21
|
+
|
|
22
|
+
const kds = (services) => new KdsService(services);
|
|
23
|
+
|
|
24
|
+
registerOfflineRoutes(router, '/api', {
|
|
25
|
+
// Активные тикеты по цехам текущего терминала.
|
|
26
|
+
'kds.getTickets': [READ, (services) => kds(services).getTicketsForTerminal(services.context.getTerminal())],
|
|
27
|
+
// Кнопка блока «Готовится»/«Готов» — статус всему тикету.
|
|
28
|
+
'kds.setTicketStatus': [MUTATION, (services, p) => kds(services).setTicketStatus(get(p, 'ticket_id'), get(p, 'status'))],
|
|
29
|
+
// Тап по блюду — статус одному item, статус тикета роллапится.
|
|
30
|
+
'kds.setItemStatus': [MUTATION, (services, p) => kds(services).setItemStatus(get(p, 'item_id'), get(p, 'status'))],
|
|
31
|
+
// Экран готовности: агрегат по открытым чекам компании.
|
|
32
|
+
'kds.getReadiness': [READ, (services) => kds(services).getReadinessOrders(services.context.companyId)],
|
|
33
|
+
// Тикеты одного чека — касса рисует KDS-статусы в списке позиций.
|
|
34
|
+
'kds.getOrderTickets': [READ, (services, p) => kds(services).getOrderTickets(services.context.companyId, get(p, 'order_id'))],
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
module.exports = router;
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
const express = require('express');
|
|
2
|
+
const router = express.Router();
|
|
3
|
+
const {registerOfflineRoutes} = require('../../../app/core');
|
|
4
|
+
const {qty, toFloat} = require('../../../app/core/support/num');
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Порт App\Http\Controllers\Api\Platform\OrderProductController — операции над позицией чека.
|
|
8
|
+
* При выключенном OFFLINE_CORE обработчики уходят в next() → запрос проксируется в Laravel.
|
|
9
|
+
*
|
|
10
|
+
* Не переносим: `get` (весь каталог продуктов) и `getExtraProducts` — это справочники,
|
|
11
|
+
* их отдаёт синхронизация снапшота, а не жизненный цикл чека.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
const get = (params, name, fallback = null) => {
|
|
15
|
+
const value = params[name];
|
|
16
|
+
return value === undefined || value === null ? fallback : value;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const MUTATION = {mutation: true};
|
|
20
|
+
const READ = {mutation: false};
|
|
21
|
+
|
|
22
|
+
registerOfflineRoutes(router, '/api', {
|
|
23
|
+
'orderProducts.getById': [READ, ({orders}, p) => orders.getById(get(p, 'id'))],
|
|
24
|
+
|
|
25
|
+
'orderProducts.setQuantity': [MUTATION, ({orderProducts}, p) => orderProducts.setQuantity({
|
|
26
|
+
orderProductId: get(p, 'id'),
|
|
27
|
+
// round(...,3) — весовые приходят дробными (кг); тот же тип, что и UnitCast,
|
|
28
|
+
// иначе строгое сравнение количества внутри сервиса никогда не совпадёт
|
|
29
|
+
quantity: qty(get(p, 'quantity', 0)),
|
|
30
|
+
})],
|
|
31
|
+
|
|
32
|
+
'orderProducts.setPrice': [MUTATION, ({orderProducts}, p) => orderProducts.setPrice({
|
|
33
|
+
orderProductId: get(p, 'id'),
|
|
34
|
+
price: toFloat(get(p, 'price', 0)),
|
|
35
|
+
})],
|
|
36
|
+
|
|
37
|
+
'orderProducts.remove': [MUTATION, ({orderProducts}, p) => orderProducts.remove(get(p, 'id'))],
|
|
38
|
+
|
|
39
|
+
'orderProducts.setComment': [MUTATION, ({orderProducts}, p) => orderProducts.setComment({
|
|
40
|
+
orderProductId: get(p, 'id'),
|
|
41
|
+
comment: get(p, 'comment'),
|
|
42
|
+
})],
|
|
43
|
+
|
|
44
|
+
'orderProducts.updateModifiers': [MUTATION, ({orderProducts}, p) => orderProducts.updateModifiers({
|
|
45
|
+
orderProductId: get(p, 'id'),
|
|
46
|
+
modifiers: get(p, 'modifiers', []),
|
|
47
|
+
})],
|
|
48
|
+
|
|
49
|
+
'orderProducts.setGuestNumber': [MUTATION, ({orderProducts}, p) => orderProducts.setGuestNumber({
|
|
50
|
+
orderProductId: get(p, 'id'),
|
|
51
|
+
guestNumber: get(p, 'guest_number'),
|
|
52
|
+
})],
|
|
53
|
+
|
|
54
|
+
'orderProducts.transfer': [MUTATION, ({orderProducts}, p) => orderProducts.transfer({
|
|
55
|
+
orderProductId: get(p, 'id'),
|
|
56
|
+
orderId: get(p, 'order_id'),
|
|
57
|
+
})],
|
|
58
|
+
|
|
59
|
+
'orderProducts.stopTimer': [MUTATION, ({orderProducts}, p) => orderProducts.stopTimer({
|
|
60
|
+
orderProductId: get(p, 'id'),
|
|
61
|
+
orderId: get(p, 'order_id'),
|
|
62
|
+
})],
|
|
63
|
+
|
|
64
|
+
'orderProducts.fixTimer': [MUTATION, ({orderProducts}, p) => orderProducts.fixTimer({
|
|
65
|
+
orderProductId: get(p, 'id'),
|
|
66
|
+
orderId: get(p, 'order_id'),
|
|
67
|
+
})],
|
|
68
|
+
|
|
69
|
+
'orderProducts.resumeTimer': [MUTATION, ({orderProducts}, p) => orderProducts.resumeTimer({
|
|
70
|
+
orderProductId: get(p, 'id'),
|
|
71
|
+
orderId: get(p, 'order_id'),
|
|
72
|
+
})],
|
|
73
|
+
|
|
74
|
+
'orderProducts.setTimerLimit': [MUTATION, ({orderProducts}, p) => orderProducts.setTimerLimit({
|
|
75
|
+
orderProductId: get(p, 'id'),
|
|
76
|
+
orderId: get(p, 'order_id'),
|
|
77
|
+
timerLimitMinutes: get(p, 'timer_limit_minutes'),
|
|
78
|
+
})],
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
module.exports = router;
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
const express = require('express');
|
|
2
|
+
const router = express.Router();
|
|
3
|
+
const {registerOfflineRoutes} = require('../../../app/core');
|
|
4
|
+
const {qty} = require('../../../app/core/support/num');
|
|
5
|
+
const PrintService = require('../../../app/core/services/PrintService');
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Порт App\Http\Controllers\Api\Platform\OrderController — жизненный цикл чека.
|
|
9
|
+
*
|
|
10
|
+
* Роуты общие (routes/common): та же логика нужна и серверу, и локальной установке.
|
|
11
|
+
* При выключенном OFFLINE_CORE каждый обработчик уходит в next() → запрос проксируется
|
|
12
|
+
* в Laravel, как сегодня.
|
|
13
|
+
*
|
|
14
|
+
* Печать (sendToWorkshops/printBillReceipt) — в PrintService: ядро решает, что и на какой
|
|
15
|
+
* принтер слать, а сам чек по-прежнему печатает терминал по сокет-запросу. Тикеты кухни
|
|
16
|
+
* (KDS) создаёт та же отправка в цех, события экрана кухни — в routes/common/api/kds.js.
|
|
17
|
+
* Склад остаётся за облаком.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
/** Аналог $request->get($name, $default): у Laravel это тело ИЛИ query. */
|
|
21
|
+
const get = (params, name, fallback = null) => {
|
|
22
|
+
const value = params[name];
|
|
23
|
+
return value === undefined || value === null ? fallback : value;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const MUTATION = {mutation: true};
|
|
27
|
+
const READ = {mutation: false};
|
|
28
|
+
|
|
29
|
+
registerOfflineRoutes(router, '/api', {
|
|
30
|
+
'orders.create': [MUTATION, ({orders}, p) => orders.create({
|
|
31
|
+
orderSourceId: get(p, 'order_source_id'),
|
|
32
|
+
tableId: get(p, 'table_id'),
|
|
33
|
+
guestCount: get(p, 'guest_count', 1),
|
|
34
|
+
hallId: get(p, 'hall_id'),
|
|
35
|
+
servingId: get(p, 'serving_id'),
|
|
36
|
+
id: get(p, 'id'),
|
|
37
|
+
slotIndex: get(p, 'slot_index', 1),
|
|
38
|
+
})],
|
|
39
|
+
|
|
40
|
+
'orders.addProduct': [MUTATION, ({orders}, p) => orders.addProduct({
|
|
41
|
+
productId: get(p, 'id'),
|
|
42
|
+
orderId: get(p, 'order_id'),
|
|
43
|
+
guestNumber: get(p, 'guest_number', 1),
|
|
44
|
+
// весовой товар присылает дробное количество (кг) — не округляем до целого
|
|
45
|
+
quantity: Math.max(qty(get(p, 'quantity', 1)), 0.001),
|
|
46
|
+
modifierSets: get(p, 'modifier_sets', []),
|
|
47
|
+
})],
|
|
48
|
+
|
|
49
|
+
'orders.addExtra': [MUTATION, ({orders}, p) => orders.addExtra({
|
|
50
|
+
name: get(p, 'name'),
|
|
51
|
+
productWorkshopId: get(p, 'product_workshop_id'),
|
|
52
|
+
price: get(p, 'price', 1),
|
|
53
|
+
orderId: get(p, 'order_id'),
|
|
54
|
+
quantity: get(p, 'quantity', 1),
|
|
55
|
+
guestNumber: get(p, 'guest_number', 1),
|
|
56
|
+
})],
|
|
57
|
+
|
|
58
|
+
'orders.changeTable': [MUTATION, ({orders}, p) => orders.changeTable({
|
|
59
|
+
orderId: get(p, 'id'),
|
|
60
|
+
tableId: get(p, 'table_id'),
|
|
61
|
+
hallId: get(p, 'hall_id'),
|
|
62
|
+
})],
|
|
63
|
+
|
|
64
|
+
'orders.changeWaiter': [MUTATION, ({orders}, p) => orders.changeWaiter({
|
|
65
|
+
orderId: get(p, 'id'),
|
|
66
|
+
waiterId: get(p, 'waiter_id'),
|
|
67
|
+
})],
|
|
68
|
+
|
|
69
|
+
'orders.changeGuestCount': [MUTATION, ({orders}, p) => orders.changeGuestCount({
|
|
70
|
+
orderId: get(p, 'id'),
|
|
71
|
+
guestCount: get(p, 'guest_count', 1),
|
|
72
|
+
})],
|
|
73
|
+
|
|
74
|
+
'orders.setComment': [MUTATION, ({orders}, p) => orders.setComment({
|
|
75
|
+
orderId: get(p, 'id'),
|
|
76
|
+
comment: get(p, 'comment', ''),
|
|
77
|
+
})],
|
|
78
|
+
|
|
79
|
+
'orders.addCustomer': [MUTATION, ({orders}, p) => orders.addCustomer(get(p, 'id'))],
|
|
80
|
+
|
|
81
|
+
'orders.removeCustomer': [MUTATION, ({orders}, p) => orders.removeCustomer({
|
|
82
|
+
orderId: get(p, 'id'),
|
|
83
|
+
guestNumber: get(p, 'number'),
|
|
84
|
+
})],
|
|
85
|
+
|
|
86
|
+
'orders.applyServiceCharge': [MUTATION, ({orders}, p) => orders.applyServiceCharge({
|
|
87
|
+
orderId: get(p, 'id'),
|
|
88
|
+
serviceCharge: get(p, 'service_charge'),
|
|
89
|
+
})],
|
|
90
|
+
|
|
91
|
+
'orders.applyDiscount': [MUTATION, ({orders}, p) => orders.applyDiscount({
|
|
92
|
+
orderId: get(p, 'id'),
|
|
93
|
+
discount: get(p, 'discount'),
|
|
94
|
+
type: get(p, 'type'),
|
|
95
|
+
})],
|
|
96
|
+
|
|
97
|
+
'orders.applyDeposit': [MUTATION, ({orders}, p) => orders.applyDeposit({
|
|
98
|
+
orderId: get(p, 'id'),
|
|
99
|
+
deposit: get(p, 'deposit_amount'),
|
|
100
|
+
})],
|
|
101
|
+
|
|
102
|
+
'orders.merge': [MUTATION, ({orders}, p) => orders.merge({
|
|
103
|
+
sourceId: get(p, 'source_id'),
|
|
104
|
+
targetId: get(p, 'target_id'),
|
|
105
|
+
})],
|
|
106
|
+
|
|
107
|
+
'orders.rollback': [MUTATION, ({orders}, p) => orders.rollback(get(p, 'id'))],
|
|
108
|
+
|
|
109
|
+
'orders.removeOrder': [MUTATION, ({orders}, p) => orders.removeOrder(get(p, 'id'))],
|
|
110
|
+
|
|
111
|
+
'orders.closeOrder': [MUTATION, ({orders}, p) => orders.closeOrder({
|
|
112
|
+
orderId: get(p, 'id'),
|
|
113
|
+
payments: get(p, 'payments', []),
|
|
114
|
+
reason: get(p, 'without_payment_reason'),
|
|
115
|
+
fiscalId: get(p, 'fiscal_id'),
|
|
116
|
+
rrn: get(p, 'rrn'),
|
|
117
|
+
})],
|
|
118
|
+
|
|
119
|
+
'orders.syncClosedOrder': [MUTATION, ({orders}, p) => orders.syncClosedOrder(get(p, 'id'))],
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Laravel берёт список позиций из кэша, который наполняет sendToWorkshops. Печать ядра
|
|
123
|
+
* работает только синхронно и синкает количество сама — кэш позиций ей не нужен и не
|
|
124
|
+
* пишется, поэтому здесь позиции выводим из самого чека: все строки заказа, чей цех есть в
|
|
125
|
+
* присланном списке. Результат тот же — синк напечатанного количества.
|
|
126
|
+
*/
|
|
127
|
+
'orders.syncWorkshops': [MUTATION, async ({orders, db}, p) => {
|
|
128
|
+
const orderId = get(p, 'id');
|
|
129
|
+
const workshops = get(p, 'workshops', []) || [];
|
|
130
|
+
const workshopIds = workshops
|
|
131
|
+
.map((workshop) => (workshop && workshop.product_workshop_id) || null)
|
|
132
|
+
.filter(Boolean);
|
|
133
|
+
|
|
134
|
+
if (!orderId || !workshopIds.length) {
|
|
135
|
+
return true;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const placeholders = workshopIds.map(() => '?').join(', ');
|
|
139
|
+
const rows = await db.all(
|
|
140
|
+
`SELECT op.id FROM order_products op
|
|
141
|
+
JOIN products p ON p.id = op.product_id
|
|
142
|
+
WHERE op.order_id = ? AND op.deleted_at IS NULL
|
|
143
|
+
AND p.product_workshop_id IN (${placeholders})`,
|
|
144
|
+
[orderId, ...workshopIds]
|
|
145
|
+
);
|
|
146
|
+
|
|
147
|
+
return orders.syncWorkshopCount(rows.map((row) => row.id), orderId);
|
|
148
|
+
}],
|
|
149
|
+
|
|
150
|
+
// Печать цеховых чеков. Print-token/Idempotency-Key Laravel нужен только async-ветке
|
|
151
|
+
// (джоб в очереди); дубль жеста ядро гасит своей idempotency, повтор состояния — дедупом
|
|
152
|
+
// печати внутри PrintService.
|
|
153
|
+
'orders.sendToWorkshops': [MUTATION, (services, p) => new PrintService(services).sendToWorkshops(get(p, 'id'))],
|
|
154
|
+
|
|
155
|
+
// Счёт/пречек. appends фронт присылает, но контроллер Laravel их не пробрасывает — тоже нет.
|
|
156
|
+
'orders.printBillReceipt': [MUTATION, (services, p) => new PrintService(services)
|
|
157
|
+
.printBillReceipt(get(p, 'id'), get(p, 'isPreReceipt', false))],
|
|
158
|
+
|
|
159
|
+
// Laravel-заглушка (return true) — оставлена для совместимости контракта.
|
|
160
|
+
'orders.syncPrintBill': [READ, () => true],
|
|
161
|
+
|
|
162
|
+
// Авторитетный список открытых чеков — ре-синк терминала после реконнекта сокета.
|
|
163
|
+
'orders.getOpen': [READ, ({orders}) => orders.getOpenOrders()],
|
|
164
|
+
|
|
165
|
+
// В Laravel ключ кэша только пишется и нигде не читается — контракт без побочного эффекта.
|
|
166
|
+
'orders.dismissTimerModifier': [READ, () => ({success: true})],
|
|
167
|
+
|
|
168
|
+
'orders.archive': [READ, ({orders}, p) => orders.archive({
|
|
169
|
+
companyId: get(p, 'company_id'),
|
|
170
|
+
date: get(p, 'date'),
|
|
171
|
+
})],
|
|
172
|
+
|
|
173
|
+
'orders.isFiscalExists': [READ, ({orders}, p) => orders.isFiscalExists(get(p, 'fiscal_id'))],
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
module.exports = router;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
const express = require('express');
|
|
2
|
+
const router = express.Router();
|
|
3
|
+
const {registerOfflineRoutes} = require('../../../app/core');
|
|
4
|
+
const PrintService = require('../../../app/core/services/PrintService');
|
|
5
|
+
const PrinterAdminService = require('../../../app/core/services/PrinterAdminService');
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Порт App\Http\Controllers\Api\Platform\PrinterController.
|
|
9
|
+
*
|
|
10
|
+
* Отдельный файл, а не routes/desktop/api/printers.js: там живут роуты самого устройства
|
|
11
|
+
* (USB-перечисление, физическая печать), а это событие POS-API — оно общее для сервера и
|
|
12
|
+
* локальной установки. При выключенном OFFLINE_CORE уходит в next() → прокси → Laravel.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
const READ = {mutation: false};
|
|
16
|
+
const MUTATION = {mutation: true};
|
|
17
|
+
|
|
18
|
+
const admin = (services) => new PrinterAdminService(services);
|
|
19
|
+
|
|
20
|
+
registerOfflineRoutes(router, '/api', {
|
|
21
|
+
// Принтер счёта текущего терминала (или null) — терминал по нему решает, печатать ли
|
|
22
|
+
// фискальный/тестовый чек сам.
|
|
23
|
+
'printers.getReceiptPrinter': [READ, (services) => new PrintService(services).getReceiptPrinter()],
|
|
24
|
+
|
|
25
|
+
// Настройка принтеров (модалка «Printerlər»). Без облака касса должна уметь назначить
|
|
26
|
+
// принтер цеху или подхватить новый IP — иначе замена принтера останавливает кухню.
|
|
27
|
+
// Ведомый пересылает мастеру: печать решает мастер, по своей БД.
|
|
28
|
+
'printers.get': [READ, (services, params) => admin(services).get(params)],
|
|
29
|
+
'printers.list': [READ, (services) => admin(services).list()],
|
|
30
|
+
'printers.update': [MUTATION, (services, params) => admin(services).update(params)],
|
|
31
|
+
'printers.delete': [MUTATION, (services, params) => admin(services).delete(params)],
|
|
32
|
+
'printers.syncAddress': [MUTATION, (services, params) => admin(services).syncAddress(params)],
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
module.exports = router;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
const express = require('express');
|
|
2
|
+
const router = express.Router();
|
|
3
|
+
const {registerOfflineRoutes} = require('../../../app/core');
|
|
4
|
+
const ReportService = require('../../../app/core/services/ReportService');
|
|
5
|
+
const syncConfig = require('../../../app/sync/config');
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Порт App\Http\Controllers\Api\Platform\ReportController — X/Z-отчёт кассы (кнопка «Hesabat»),
|
|
9
|
+
* без облака. В режиме `cloud` синка событие, как и раньше, считает Laravel.
|
|
10
|
+
*
|
|
11
|
+
* При выключенном OFFLINE_CORE (или событии вне CORE_EVENTS) уходит в next() → Laravel.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
const READ = {mutation: false};
|
|
15
|
+
|
|
16
|
+
// Граница полной истории кассы. require ленивый: синк сам тянет ядро.
|
|
17
|
+
const history = (services) => {
|
|
18
|
+
if (!syncConfig.isSyncEnabled()) {
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
const companyId = services.context.companyId;
|
|
22
|
+
return () => require('../../../app/sync').history(companyId);
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
registerOfflineRoutes(router, '/api', {
|
|
26
|
+
'reports.getReport': [READ, (services, params) => new ReportService({...services, history: history(services)}).getReport(params)],
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
module.exports = router;
|
package/routes/common/socket.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
const express = require('express');
|
|
2
|
-
const {socketSendByFilter
|
|
2
|
+
const {socketSendByFilter} = require("../../utils");
|
|
3
|
+
const {socketRequest} = require("../../app/socket/request");
|
|
3
4
|
const serverSocket = require("../../app/socket/server")();
|
|
4
5
|
const router = express.Router();
|
|
5
6
|
|
|
@@ -51,41 +52,20 @@ router.all('/socket', (req, res) => {
|
|
|
51
52
|
});
|
|
52
53
|
});
|
|
53
54
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
return res.json({
|
|
67
|
-
success: false,
|
|
68
|
-
message: 'No socket',
|
|
69
|
-
}).status(404);
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
socket.emit(requestEventName, data);
|
|
73
|
-
socket.once(responseEventName, (data) => {
|
|
74
|
-
if (!isSent) {
|
|
75
|
-
isSent = true;
|
|
76
|
-
res.json(data);
|
|
77
|
-
}
|
|
55
|
+
/**
|
|
56
|
+
* Запрос к терминалу с ожиданием ответа (печать из Laravel). Логика — в app/socket/request.js,
|
|
57
|
+
* там же ей пользуется офлайн-ядро. Формат ответа прежний, HTTP-статус как был — 200:
|
|
58
|
+
* `.status()` после `res.json()` здесь никогда не срабатывал, и Laravel смотрит только в тело.
|
|
59
|
+
*/
|
|
60
|
+
router.post('/socket/request', async (req, res) => {
|
|
61
|
+
const response = await socketRequest({
|
|
62
|
+
eventName: req.body?.event ?? null,
|
|
63
|
+
data: req.body?.data ?? null,
|
|
64
|
+
requestId: req.body.request_id,
|
|
65
|
+
terminalId: req.body?.terminal_id ?? '9ec525b4-7e0b-422b-a6e1-486290578c8c',
|
|
66
|
+
timeout: req.body.time_out ?? 10000,
|
|
78
67
|
});
|
|
79
|
-
|
|
80
|
-
setTimeout(() => {
|
|
81
|
-
if (!isSent) {
|
|
82
|
-
isSent = true;
|
|
83
|
-
res.json({
|
|
84
|
-
success: false,
|
|
85
|
-
message: 'Timeout error'
|
|
86
|
-
}).status(408)
|
|
87
|
-
}
|
|
88
|
-
}, timeOut)
|
|
68
|
+
res.json(response);
|
|
89
69
|
})
|
|
90
70
|
|
|
91
71
|
module.exports = router;
|
package/routes/common/tax.js
CHANGED
|
@@ -4,17 +4,20 @@ const serverSocket = require("../../app/socket/server")();
|
|
|
4
4
|
const router = express.Router();
|
|
5
5
|
const {createProxyMiddleware, fixRequestBody} = require("http-proxy-middleware");
|
|
6
6
|
const moment = require("moment/moment");
|
|
7
|
+
const {taxTarget, taxProxyEnabled} = require("../../app/security");
|
|
8
|
+
const {LAN_KEY_HEADER} = require("../../app/sync/config");
|
|
9
|
+
|
|
10
|
+
const DEVICE_AUTH_HEADERS = ['authorization', 'terminal-id', 'company-id', 'team-id', 'employee-id'];
|
|
7
11
|
|
|
8
12
|
const proxy = createProxyMiddleware({
|
|
9
13
|
target: 'http://localhost:3005',
|
|
10
14
|
preserveHeaderKeyCase: true,
|
|
11
15
|
router: function (req) {
|
|
12
|
-
//
|
|
13
|
-
const
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
req._targetPath = pathname; // сохраняем путь, чтобы использовать в pathRewrite
|
|
16
|
+
// URL уже проверен в taxGuard (из query или тела запроса)
|
|
17
|
+
const { protocol, host, pathname, search } = req._taxTarget;
|
|
18
|
+
// Сохраняем хост и протокол как target (без пути). Query цели едет вместе с путём:
|
|
19
|
+
// раньше он терялся, и аппарат с параметрами в адресе получал запрос без них.
|
|
20
|
+
req._targetPath = pathname + search; // сохраняем путь, чтобы использовать в pathRewrite
|
|
18
21
|
return `${protocol}//${host}`;
|
|
19
22
|
},
|
|
20
23
|
pathRewrite: function (path, req) {
|
|
@@ -22,11 +25,36 @@ const proxy = createProxyMiddleware({
|
|
|
22
25
|
},
|
|
23
26
|
on: {
|
|
24
27
|
proxyReq: (proxyReq, req, res, options) => {
|
|
28
|
+
// Ключ зала и токен телефона (LAN-переход, app/sync/lan.js) — для мастера, не для аппарата.
|
|
29
|
+
proxyReq.removeHeader(LAN_KEY_HEADER);
|
|
30
|
+
if (req.lanDevice) {
|
|
31
|
+
DEVICE_AUTH_HEADERS.forEach((name) => proxyReq.removeHeader(name));
|
|
32
|
+
}
|
|
33
|
+
// Тело уже распарсено body-parser'ом (нужно для req.body.url) и будет пересобрано
|
|
34
|
+
// fixRequestBody с Content-Length. Снимаем chunked, чтобы в апстрим не ушли
|
|
35
|
+
// Transfer-Encoding и Content-Length одновременно (иначе 400 Bad Request).
|
|
36
|
+
if (req.readableLength === 0 && req.body && Object.keys(req.body).length > 0) {
|
|
37
|
+
proxyReq.removeHeader('transfer-encoding');
|
|
38
|
+
}
|
|
25
39
|
return fixRequestBody(proxyReq, req);
|
|
26
40
|
},
|
|
27
41
|
}
|
|
28
42
|
})
|
|
29
43
|
|
|
30
|
-
|
|
44
|
+
// Цель прокси приходит из запроса — без проверки это открытый SSRF (app/security.js).
|
|
45
|
+
// На облачном Node прокси выключен (TAX_PROXY_ENABLED): аппараты стоят в сети кассы.
|
|
46
|
+
const taxGuard = (req, res, next) => {
|
|
47
|
+
if (!taxProxyEnabled()) {
|
|
48
|
+
return res.status(404).json({success: false, message: 'Not found'});
|
|
49
|
+
}
|
|
50
|
+
const target = taxTarget(req.query.url || req.body?.url);
|
|
51
|
+
if (!target) {
|
|
52
|
+
return res.status(400).json({success: false, message: 'Invalid target url'});
|
|
53
|
+
}
|
|
54
|
+
req._taxTarget = target;
|
|
55
|
+
next();
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
router.use('/tax', taxGuard, proxy)
|
|
31
59
|
|
|
32
60
|
module.exports = router;
|