@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.
- 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 +117 -0
- package/app/core/resources/casts.js +55 -0
- package/app/core/services/AuthService.js +468 -0
- package/app/core/services/InitService.js +279 -0
- package/app/core/services/KdsService.js +639 -0
- package/app/core/services/OrderProductService.js +309 -0
- package/app/core/services/OrderService.js +911 -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 +11 -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 +29 -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 +72 -3
- package/utils.js +6 -0
- package/tests/usbDiagnose.js +0 -59
- package/tests/usbPrinterTest.js +0 -20
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
const OrderProduct = require('../models/OrderProduct');
|
|
2
|
+
const SnapshotRepository = require('./SnapshotRepository');
|
|
3
|
+
const {nowSql, toSql} = require('../support/time');
|
|
4
|
+
const {uuid} = require('../support/ids');
|
|
5
|
+
const {castRow, castColumn} = require('../support/db');
|
|
6
|
+
|
|
7
|
+
const TYPE_TIMER = OrderProduct.TYPE_TIMER;
|
|
8
|
+
|
|
9
|
+
class OrderProductRepository {
|
|
10
|
+
constructor(db) {
|
|
11
|
+
this.db = db;
|
|
12
|
+
this.snapshot = new SnapshotRepository(db);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
async findRow(id) {
|
|
16
|
+
if (!id) {
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
return this.db.get('SELECT * FROM order_products WHERE id = ? AND deleted_at IS NULL', [id]);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
async find(id) {
|
|
23
|
+
const row = await this.findRow(id);
|
|
24
|
+
if (!row) {
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
const [product, modifiers] = await Promise.all([
|
|
28
|
+
this.snapshot.findProduct(row.product_id),
|
|
29
|
+
this.modifiersOf(row.id),
|
|
30
|
+
]);
|
|
31
|
+
return new OrderProduct(row, {product, modifiers});
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async modifiersOf(orderProductId) {
|
|
35
|
+
return this.db.all(
|
|
36
|
+
'SELECT * FROM order_product_modifier_sets WHERE order_product_id = ? AND deleted_at IS NULL',
|
|
37
|
+
[orderProductId]
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Поиск позиции, в которую можно долить количество (склейка строк чека).
|
|
43
|
+
* Ключ склейки — order_id + product_id + guest_number + хэш модификаторов; повременные
|
|
44
|
+
* позиции не склеиваются никогда.
|
|
45
|
+
*
|
|
46
|
+
* @param {boolean} appendMode settings.terminal_settings.append_order_product — при нём
|
|
47
|
+
* нельзя доливать в уже отправленную на кухню строку, иначе цех не увидит добавку.
|
|
48
|
+
*/
|
|
49
|
+
async findMergeCandidate({orderId, productId, guestNumber, hash, appendMode = false}) {
|
|
50
|
+
const params = [orderId, productId, guestNumber, TYPE_TIMER];
|
|
51
|
+
let sql = `SELECT * FROM order_products
|
|
52
|
+
WHERE order_id = ? AND product_id = ? AND guest_number = ?
|
|
53
|
+
AND type != ? AND deleted_at IS NULL`;
|
|
54
|
+
|
|
55
|
+
if (hash === null || hash === undefined) {
|
|
56
|
+
sql += ' AND modifications_sets_hash IS NULL';
|
|
57
|
+
} else {
|
|
58
|
+
sql += ' AND modifications_sets_hash = ?';
|
|
59
|
+
params.push(hash);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (appendMode) {
|
|
63
|
+
sql += ' AND quantity != workshop_print_quantity';
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
sql += ' LIMIT 1';
|
|
67
|
+
const row = await this.db.get(sql, params);
|
|
68
|
+
return row ? this.find(row.id) : null;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** Позиция того же состава у другого гостя — нужна setGuestNumber, чтобы слить строки. */
|
|
72
|
+
async findSameProductForGuest({orderId, productId, guestNumber, price, hash, excludeId}) {
|
|
73
|
+
const params = [orderId, productId, guestNumber, price, excludeId];
|
|
74
|
+
let sql = `SELECT * FROM order_products
|
|
75
|
+
WHERE order_id = ? AND product_id = ? AND guest_number = ? AND price = ?
|
|
76
|
+
AND id != ? AND deleted_at IS NULL`;
|
|
77
|
+
if (hash === null || hash === undefined) {
|
|
78
|
+
sql += ' AND modifications_sets_hash IS NULL';
|
|
79
|
+
} else {
|
|
80
|
+
sql += ' AND modifications_sets_hash = ?';
|
|
81
|
+
params.push(hash);
|
|
82
|
+
}
|
|
83
|
+
sql += ' LIMIT 1';
|
|
84
|
+
const row = await this.db.get(sql, params);
|
|
85
|
+
return row ? this.find(row.id) : null;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
async insert(data) {
|
|
89
|
+
const timestamp = nowSql();
|
|
90
|
+
const id = data.id || uuid();
|
|
91
|
+
const payload = {
|
|
92
|
+
id,
|
|
93
|
+
order_id: data.order_id,
|
|
94
|
+
product_id: data.product_id,
|
|
95
|
+
company_id: data.company_id,
|
|
96
|
+
team_id: data.team_id,
|
|
97
|
+
unit_id: data.unit_id ?? null,
|
|
98
|
+
guest_number: data.guest_number ?? 1,
|
|
99
|
+
price: data.price ?? 0,
|
|
100
|
+
cost: data.cost ?? 0,
|
|
101
|
+
profit: data.profit ?? 0,
|
|
102
|
+
cost_total: data.cost_total ?? 0,
|
|
103
|
+
price_total: data.price_total ?? 0,
|
|
104
|
+
quantity: data.quantity ?? 1,
|
|
105
|
+
price_after_service_charge: data.price_after_service_charge ?? 0,
|
|
106
|
+
price_after_discount: data.price_after_discount ?? 0,
|
|
107
|
+
subtotal: data.subtotal ?? 0,
|
|
108
|
+
discount: data.discount ?? 0,
|
|
109
|
+
discount_amount: data.discount_amount ?? 0,
|
|
110
|
+
total: data.total ?? 0,
|
|
111
|
+
service_charge: data.service_charge ?? 0,
|
|
112
|
+
service_charge_amount: data.service_charge_amount ?? 0,
|
|
113
|
+
max_discount_amount: data.max_discount_amount ?? 0,
|
|
114
|
+
// Laravel эти колонки при вставке не заполняет — значение берётся из DEFAULT
|
|
115
|
+
// схемы. В общей базе они NOT NULL, поэтому явный null сюда слать нельзя:
|
|
116
|
+
// повторяем те же дефолты.
|
|
117
|
+
service_charge_calculate_mode: data.service_charge_calculate_mode ?? 'after_discount',
|
|
118
|
+
discount_type: data.discount_type ?? 'percent',
|
|
119
|
+
status: data.status ?? 'cooking',
|
|
120
|
+
type: data.type ?? OrderProduct.TYPE_PRODUCT,
|
|
121
|
+
added_at: toSql(data.added_at) || timestamp,
|
|
122
|
+
modified_at: toSql(data.modified_at),
|
|
123
|
+
is_gift: data.is_gift ?? 0,
|
|
124
|
+
stopped_at: toSql(data.stopped_at),
|
|
125
|
+
is_stopped: data.is_stopped ?? 0,
|
|
126
|
+
is_fixed: data.is_fixed ?? 0,
|
|
127
|
+
paused_seconds: data.paused_seconds ?? 0,
|
|
128
|
+
timer_limit_minutes: data.timer_limit_minutes ?? null,
|
|
129
|
+
comment: data.comment ?? null,
|
|
130
|
+
modifications_sets_hash: data.modifications_sets_hash ?? null,
|
|
131
|
+
print_quantity: data.print_quantity ?? 0,
|
|
132
|
+
receipt_print_quantity: data.receipt_print_quantity ?? 0,
|
|
133
|
+
workshop_print_quantity: data.workshop_print_quantity ?? 0,
|
|
134
|
+
kds_print_quantity: data.kds_print_quantity ?? 0,
|
|
135
|
+
total_workshop_print_quantity: data.total_workshop_print_quantity ?? 0,
|
|
136
|
+
created_at: timestamp,
|
|
137
|
+
updated_at: timestamp,
|
|
138
|
+
};
|
|
139
|
+
await this.db.insert('order_products', castRow(payload));
|
|
140
|
+
return id;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
async update(id, data) {
|
|
144
|
+
const payload = {};
|
|
145
|
+
for (const [column, value] of Object.entries(data)) {
|
|
146
|
+
if (['added_at', 'modified_at', 'stopped_at'].includes(column)) {
|
|
147
|
+
payload[column] = toSql(value);
|
|
148
|
+
} else {
|
|
149
|
+
payload[column] = castColumn(column, value === undefined ? null : value);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
payload.updated_at = nowSql();
|
|
153
|
+
return this.db.update('order_products', id, payload);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
async softDelete(id) {
|
|
157
|
+
const timestamp = nowSql();
|
|
158
|
+
await this.db.update('order_products', id, {deleted_at: timestamp, updated_at: timestamp});
|
|
159
|
+
await this.db.run(
|
|
160
|
+
'UPDATE order_product_modifier_sets SET deleted_at = ?, updated_at = ? WHERE order_product_id = ? AND deleted_at IS NULL',
|
|
161
|
+
[timestamp, timestamp, id]
|
|
162
|
+
);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/** Итоговая чистка после syncWorkshopCount: строки, которых больше нет ни в чеке, ни в печати. */
|
|
166
|
+
async deleteEmpty(orderId) {
|
|
167
|
+
const rows = await this.db.all(
|
|
168
|
+
`SELECT id FROM order_products
|
|
169
|
+
WHERE order_id = ? AND quantity <= 0 AND workshop_print_quantity <= 0 AND deleted_at IS NULL`,
|
|
170
|
+
[orderId]
|
|
171
|
+
);
|
|
172
|
+
for (const row of rows) {
|
|
173
|
+
await this.softDelete(row.id);
|
|
174
|
+
}
|
|
175
|
+
return rows.length;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
async moveToOrder(sourceOrderId, targetOrderId) {
|
|
179
|
+
return this.db.run(
|
|
180
|
+
'UPDATE order_products SET order_id = ?, updated_at = ? WHERE order_id = ? AND deleted_at IS NULL',
|
|
181
|
+
[targetOrderId, nowSql(), sourceOrderId]
|
|
182
|
+
);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
async removeGuestProducts(orderId, guestNumber) {
|
|
186
|
+
const rows = await this.db.all(
|
|
187
|
+
'SELECT id FROM order_products WHERE order_id = ? AND guest_number = ? AND deleted_at IS NULL',
|
|
188
|
+
[orderId, guestNumber]
|
|
189
|
+
);
|
|
190
|
+
for (const row of rows) {
|
|
191
|
+
await this.softDelete(row.id);
|
|
192
|
+
}
|
|
193
|
+
return rows.length;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
async insertModifierSets(orderProductId, sets) {
|
|
197
|
+
const timestamp = nowSql();
|
|
198
|
+
for (const set of sets) {
|
|
199
|
+
await this.db.insert('order_product_modifier_sets', {
|
|
200
|
+
id: uuid(),
|
|
201
|
+
order_product_id: orderProductId,
|
|
202
|
+
modifier_set_id: set.modifier_set_id ?? null,
|
|
203
|
+
modifier_set_modifier_id: set.modifier_set_modifier_id ?? null,
|
|
204
|
+
modifier_set_type: set.modifier_set_type ?? null,
|
|
205
|
+
modifier_set_title: set.modifier_set_title ?? null,
|
|
206
|
+
name: set.name ?? null,
|
|
207
|
+
price: set.price ?? 0,
|
|
208
|
+
quantity: set.quantity ?? 0,
|
|
209
|
+
total: set.total ?? 0,
|
|
210
|
+
created_at: timestamp,
|
|
211
|
+
updated_at: timestamp,
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
async deleteModifierSets(orderProductId) {
|
|
217
|
+
const timestamp = nowSql();
|
|
218
|
+
return this.db.run(
|
|
219
|
+
'UPDATE order_product_modifier_sets SET deleted_at = ?, updated_at = ? WHERE order_product_id = ? AND deleted_at IS NULL',
|
|
220
|
+
[timestamp, timestamp, orderProductId]
|
|
221
|
+
);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
module.exports = OrderProductRepository;
|
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
const Order = require('../models/Order');
|
|
2
|
+
const OrderProduct = require('../models/OrderProduct');
|
|
3
|
+
const SnapshotRepository = require('./SnapshotRepository');
|
|
4
|
+
const {toSql, nowSql} = require('../support/time');
|
|
5
|
+
const {phpJsonEncode} = require('../support/str');
|
|
6
|
+
const {castColumn} = require('../support/db');
|
|
7
|
+
|
|
8
|
+
// Колонки, которые ядро пишет само. Список явный: SELECT * читаем, а вот при записи
|
|
9
|
+
// молчаливый «сохраним всё, что оказалось на объекте» уронил бы UPDATE на первом же
|
|
10
|
+
// вычисляемом поле вроде elapsed_total_minutes.
|
|
11
|
+
const ORDER_COLUMNS = [
|
|
12
|
+
'global_number', 'number', 'opened_at', 'closed_at', 'progress', 'guest_count', 'comment',
|
|
13
|
+
'status', 'payed_by_third_party_application', 'profit', 'cost', 'fiscal_id', 'fiscal_url',
|
|
14
|
+
'rrn', 'subtotal', 'service_charge', 'service_charge_amount', 'deposit_type', 'deposit_amount',
|
|
15
|
+
'deposit_gap', 'discount_amount', 'discount', 'max_discount_amount', 'discount_type', 'tax',
|
|
16
|
+
'tip', 'total', 'is_closed_without_payment', 'without_payment_reason', 'data', 'customers',
|
|
17
|
+
'extras', 'closing_order_started_at', 'client_id', 'table_id', 'hall_id', 'team_id',
|
|
18
|
+
'company_id', 'terminal_id', 'order_source_id', 'opened_by_id', 'closed_by_id', 'serving_id',
|
|
19
|
+
'shift_id', 'slot_index', 'printed_at', 'should_send_to_kitchen',
|
|
20
|
+
];
|
|
21
|
+
|
|
22
|
+
const ORDER_PRODUCT_COLUMNS = [
|
|
23
|
+
'order_id', 'product_id', 'company_id', 'team_id', 'unit_id', 'guest_number', 'price', 'cost',
|
|
24
|
+
'profit', 'cost_total', 'price_total', 'quantity', 'price_after_service_charge',
|
|
25
|
+
'price_after_discount', 'subtotal', 'discount', 'discount_amount', 'total', 'service_charge',
|
|
26
|
+
'service_charge_amount', 'max_discount_amount', 'service_charge_calculate_mode',
|
|
27
|
+
'discount_type', 'status', 'type', 'added_at', 'modified_at', 'is_gift', 'stopped_at',
|
|
28
|
+
'is_stopped', 'is_fixed', 'paused_seconds', 'timer_limit_minutes', 'comment',
|
|
29
|
+
'modifications_sets_hash', 'print_quantity', 'receipt_print_quantity',
|
|
30
|
+
'workshop_print_quantity', 'total_workshop_print_quantity',
|
|
31
|
+
// kds_print_quantity намеренно НЕ здесь: его пишет только KdsService, точечным UPDATE.
|
|
32
|
+
// Eloquent сохраняет лишь изменённые поля, а saveProduct пишет весь список — сохранение
|
|
33
|
+
// модели, прочитанной до отправки на кухню, вернуло бы старое значение, и следующая
|
|
34
|
+
// отправка создала бы тот же тикет второй раз.
|
|
35
|
+
];
|
|
36
|
+
|
|
37
|
+
const DATE_COLUMNS = new Set([
|
|
38
|
+
'opened_at', 'closed_at', 'closing_order_started_at', 'printed_at', 'added_at',
|
|
39
|
+
'modified_at', 'stopped_at',
|
|
40
|
+
]);
|
|
41
|
+
|
|
42
|
+
const serializeValue = (column, value) => {
|
|
43
|
+
if (value === undefined) {
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
if (DATE_COLUMNS.has(column)) {
|
|
47
|
+
return toSql(value);
|
|
48
|
+
}
|
|
49
|
+
if (column === 'customers' || column === 'data' || column === 'extras') {
|
|
50
|
+
if (value === null) {
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
return typeof value === 'string' ? value : phpJsonEncode(value);
|
|
54
|
+
}
|
|
55
|
+
return castColumn(column, value);
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
const pickColumns = (source, columns) => {
|
|
59
|
+
const data = {};
|
|
60
|
+
for (const column of columns) {
|
|
61
|
+
if (source[column] !== undefined) {
|
|
62
|
+
data[column] = serializeValue(column, source[column]);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return data;
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
class OrderRepository {
|
|
69
|
+
constructor(db, context = null) {
|
|
70
|
+
this.db = db;
|
|
71
|
+
this.context = context;
|
|
72
|
+
this.snapshot = new SnapshotRepository(db);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async findRow(id, {withTrashed = false} = {}) {
|
|
76
|
+
if (!id) {
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
const trashed = withTrashed ? '' : ' AND deleted_at IS NULL';
|
|
80
|
+
return this.db.get(`SELECT * FROM orders WHERE id = ?${trashed}`, [id]);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** Чек со всеми связями, которые нужны расчёту и ресурсу (аналог orderRelations()). */
|
|
84
|
+
async find(id, {withTrashed = false} = {}) {
|
|
85
|
+
const row = await this.findRow(id, {withTrashed});
|
|
86
|
+
return row ? this.hydrate(row) : null;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
async openOrders(companyId) {
|
|
90
|
+
const rows = await this.db.all(
|
|
91
|
+
`SELECT * FROM orders
|
|
92
|
+
WHERE company_id = ? AND status NOT IN ('closing', 'closed') AND deleted_at IS NULL
|
|
93
|
+
ORDER BY created_at`,
|
|
94
|
+
[companyId]
|
|
95
|
+
);
|
|
96
|
+
return Promise.all(rows.map((row) => this.hydrate(row)));
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
async archivedOrders(companyId, fromSql, toSql_) {
|
|
100
|
+
const rows = await this.db.all(
|
|
101
|
+
`SELECT * FROM orders
|
|
102
|
+
WHERE company_id = ? AND closed_at IS NOT NULL AND closed_at BETWEEN ? AND ?
|
|
103
|
+
AND deleted_at IS NULL
|
|
104
|
+
ORDER BY closed_at DESC`,
|
|
105
|
+
[companyId, fromSql, toSql_]
|
|
106
|
+
);
|
|
107
|
+
return Promise.all(rows.map((row) => this.hydrate(row)));
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** Открытые чеки с работающим таймером — для tickTimers. */
|
|
111
|
+
/**
|
|
112
|
+
* Открытые чеки с идущими повременными позициями.
|
|
113
|
+
*
|
|
114
|
+
* @param {string|null} teamId только эта команда (облако тикает по командам: у каждой
|
|
115
|
+
* свои настройки сервисного сбора и язык)
|
|
116
|
+
* @param {Iterable<string>} skipCompanyIds заведения, которые тикает мастер-касса (модель M)
|
|
117
|
+
*/
|
|
118
|
+
async ordersWithRunningTimers({teamId = null, skipCompanyIds = []} = {}) {
|
|
119
|
+
let sql = `SELECT DISTINCT o.* FROM orders o
|
|
120
|
+
JOIN order_products op ON op.order_id = o.id AND op.deleted_at IS NULL
|
|
121
|
+
WHERE o.status NOT IN ('closing', 'closed') AND o.printed_at IS NULL AND o.deleted_at IS NULL
|
|
122
|
+
AND op.type = 'timer' AND op.is_stopped = ?`;
|
|
123
|
+
const params = [false];
|
|
124
|
+
if (teamId) {
|
|
125
|
+
sql += ' AND o.team_id = ?';
|
|
126
|
+
params.push(teamId);
|
|
127
|
+
}
|
|
128
|
+
const skip = [...skipCompanyIds];
|
|
129
|
+
if (skip.length) {
|
|
130
|
+
sql += ` AND o.company_id NOT IN (${skip.map(() => '?').join(', ')})`;
|
|
131
|
+
params.push(...skip);
|
|
132
|
+
}
|
|
133
|
+
const rows = await this.db.all(sql, params);
|
|
134
|
+
return Promise.all(rows.map((row) => this.hydrate(row)));
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** Команды, у которых сейчас идут повременные позиции (облачный тик). */
|
|
138
|
+
async teamsWithRunningTimers() {
|
|
139
|
+
const rows = await this.db.all(
|
|
140
|
+
`SELECT DISTINCT o.team_id FROM orders o
|
|
141
|
+
JOIN order_products op ON op.order_id = o.id AND op.deleted_at IS NULL
|
|
142
|
+
WHERE o.status NOT IN ('closing', 'closed') AND o.printed_at IS NULL AND o.deleted_at IS NULL
|
|
143
|
+
AND op.type = 'timer' AND op.is_stopped = ?`,
|
|
144
|
+
[false]
|
|
145
|
+
);
|
|
146
|
+
return rows.map((row) => row.team_id).filter(Boolean).map(String);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
async hydrate(row) {
|
|
150
|
+
const [products, payments, source, table, hall, waiter] = await Promise.all([
|
|
151
|
+
this.loadProducts(row.id),
|
|
152
|
+
this.db.all(
|
|
153
|
+
'SELECT * FROM order_payments WHERE order_id = ? AND deleted_at IS NULL ORDER BY amount',
|
|
154
|
+
[row.id]
|
|
155
|
+
),
|
|
156
|
+
this.snapshot.findOrderSourceById(row.order_source_id),
|
|
157
|
+
this.snapshot.findTable(row.table_id),
|
|
158
|
+
this.snapshot.findHall(row.hall_id),
|
|
159
|
+
this.snapshot.findTeamUserWithUser(row.serving_id),
|
|
160
|
+
]);
|
|
161
|
+
|
|
162
|
+
const order = new Order(row, {products, payments, source, table, hall, waiter});
|
|
163
|
+
order.serviceChargeMode = this.context ? this.context.serviceChargeMode() : 'after_discount';
|
|
164
|
+
return order;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
async loadProducts(orderId) {
|
|
168
|
+
const rows = await this.db.all(
|
|
169
|
+
'SELECT * FROM order_products WHERE order_id = ? AND deleted_at IS NULL ORDER BY created_at',
|
|
170
|
+
[orderId]
|
|
171
|
+
);
|
|
172
|
+
return Promise.all(rows.map((productRow) => this.hydrateProduct(productRow)));
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
async hydrateProduct(row) {
|
|
176
|
+
const [modifiers, product] = await Promise.all([
|
|
177
|
+
this.db.all(
|
|
178
|
+
'SELECT * FROM order_product_modifier_sets WHERE order_product_id = ? AND deleted_at IS NULL',
|
|
179
|
+
[row.id]
|
|
180
|
+
),
|
|
181
|
+
this.snapshot.findProduct(row.product_id),
|
|
182
|
+
]);
|
|
183
|
+
return new OrderProduct(row, {product, modifiers});
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
async insert(data) {
|
|
187
|
+
const timestamp = nowSql();
|
|
188
|
+
const payload = {
|
|
189
|
+
id: data.id,
|
|
190
|
+
...pickColumns(data, ORDER_COLUMNS),
|
|
191
|
+
created_at: timestamp,
|
|
192
|
+
updated_at: timestamp,
|
|
193
|
+
};
|
|
194
|
+
await this.db.insert('orders', payload);
|
|
195
|
+
return data.id;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/** Сохранение чека вместе с позициями — калька на `$order->calculate(...)->save()`. */
|
|
199
|
+
async save(order, {withProducts = true} = {}) {
|
|
200
|
+
const data = pickColumns(order, ORDER_COLUMNS);
|
|
201
|
+
data.updated_at = nowSql();
|
|
202
|
+
await this.db.update('orders', order.id, data);
|
|
203
|
+
|
|
204
|
+
if (withProducts) {
|
|
205
|
+
await this.saveProducts(order);
|
|
206
|
+
}
|
|
207
|
+
return order;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
async saveProducts(order) {
|
|
211
|
+
for (const product of order.products) {
|
|
212
|
+
await this.saveProduct(product);
|
|
213
|
+
}
|
|
214
|
+
return order;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
async saveProduct(product) {
|
|
218
|
+
const data = pickColumns(product, ORDER_PRODUCT_COLUMNS);
|
|
219
|
+
data.updated_at = nowSql();
|
|
220
|
+
await this.db.update('order_products', product.id, data);
|
|
221
|
+
return product;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
async updateRow(id, data) {
|
|
225
|
+
const payload = {};
|
|
226
|
+
for (const [column, value] of Object.entries(data)) {
|
|
227
|
+
payload[column] = serializeValue(column, value);
|
|
228
|
+
}
|
|
229
|
+
payload.updated_at = nowSql();
|
|
230
|
+
return this.db.update('orders', id, payload);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
async softDelete(id) {
|
|
234
|
+
return this.db.update('orders', id, {deleted_at: nowSql(), updated_at: nowSql()});
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
async countProducts(orderId) {
|
|
238
|
+
const count = await this.db.value(
|
|
239
|
+
'SELECT COUNT(*) AS count FROM order_products WHERE order_id = ? AND deleted_at IS NULL',
|
|
240
|
+
[orderId]
|
|
241
|
+
);
|
|
242
|
+
return Number(count) || 0;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* global_number сквозной по компании, number — либо сквозной, либо в пределах бизнес-дня
|
|
247
|
+
* (settings.receipts.reset_number). Офлайн это же место станет выдачей из резерва диапазонов.
|
|
248
|
+
*/
|
|
249
|
+
async nextNumbers(companyId, {resetNumber = false, dayFrom = null, dayTo = null} = {}) {
|
|
250
|
+
const last = await this.db.get(
|
|
251
|
+
'SELECT global_number, number FROM orders WHERE company_id = ? ORDER BY global_number DESC LIMIT 1',
|
|
252
|
+
[companyId]
|
|
253
|
+
);
|
|
254
|
+
|
|
255
|
+
let number;
|
|
256
|
+
if (resetNumber && dayFrom && dayTo) {
|
|
257
|
+
const lastDayNumber = await this.db.value(
|
|
258
|
+
'SELECT MAX(number) AS number FROM orders WHERE company_id = ? AND opened_at BETWEEN ? AND ?',
|
|
259
|
+
[companyId, dayFrom, dayTo]
|
|
260
|
+
);
|
|
261
|
+
number = lastDayNumber ? lastDayNumber + 1 : 1;
|
|
262
|
+
} else {
|
|
263
|
+
number = last ? (last.number === 0 ? 1 : last.number + 1) : 1;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
const globalNumber = last ? (last.global_number ? last.global_number + 1 : 1) : 1;
|
|
267
|
+
return {number, globalNumber};
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
OrderRepository.ORDER_COLUMNS = ORDER_COLUMNS;
|
|
272
|
+
OrderRepository.ORDER_PRODUCT_COLUMNS = ORDER_PRODUCT_COLUMNS;
|
|
273
|
+
|
|
274
|
+
module.exports = OrderRepository;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
const {nowSql} = require('../support/time');
|
|
2
|
+
const {uuid} = require('../support/ids');
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Очередь мутаций наверх. Пишем сразу при обработке запроса, отправляем отдельным
|
|
6
|
+
* воркером — здесь только хранилище, чтобы ядро не зависело от транспорта.
|
|
7
|
+
*/
|
|
8
|
+
class OutboxRepository {
|
|
9
|
+
constructor(db) {
|
|
10
|
+
this.db = db;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
async push({event, payload, headers = null, operationId = null}) {
|
|
14
|
+
const id = uuid();
|
|
15
|
+
await this.db.insert('outbox', {
|
|
16
|
+
id,
|
|
17
|
+
operation_id: operationId,
|
|
18
|
+
event,
|
|
19
|
+
payload: typeof payload === 'string' ? payload : JSON.stringify(payload),
|
|
20
|
+
headers: headers ? (typeof headers === 'string' ? headers : JSON.stringify(headers)) : null,
|
|
21
|
+
status: 'pending',
|
|
22
|
+
attempts: 0,
|
|
23
|
+
last_error: null,
|
|
24
|
+
created_at: nowSql(),
|
|
25
|
+
sent_at: null,
|
|
26
|
+
});
|
|
27
|
+
return id;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async pending(limit = 50) {
|
|
31
|
+
return this.db.all(
|
|
32
|
+
"SELECT * FROM outbox WHERE status = 'pending' ORDER BY created_at LIMIT ?",
|
|
33
|
+
[limit]
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async markSent(id) {
|
|
38
|
+
return this.db.run(
|
|
39
|
+
"UPDATE outbox SET status = 'sent', sent_at = ? WHERE id = ?",
|
|
40
|
+
[nowSql(), id]
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async markFailed(id, error) {
|
|
45
|
+
return this.db.run(
|
|
46
|
+
'UPDATE outbox SET attempts = attempts + 1, last_error = ? WHERE id = ?',
|
|
47
|
+
[String(error).slice(0, 1000), id]
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
module.exports = OutboxRepository;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
const {nowSql} = require('../support/time');
|
|
2
|
+
const {uuid} = require('../support/ids');
|
|
3
|
+
|
|
4
|
+
class PaymentRepository {
|
|
5
|
+
constructor(db) {
|
|
6
|
+
this.db = db;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
async ofOrder(orderId) {
|
|
10
|
+
return this.db.all(
|
|
11
|
+
'SELECT * FROM order_payments WHERE order_id = ? AND deleted_at IS NULL ORDER BY amount',
|
|
12
|
+
[orderId]
|
|
13
|
+
);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** closeOrder переписывает оплаты целиком — старые сносим, новые пишем заново. */
|
|
17
|
+
async deleteOfOrder(orderId) {
|
|
18
|
+
const timestamp = nowSql();
|
|
19
|
+
return this.db.run(
|
|
20
|
+
'UPDATE order_payments SET deleted_at = ?, updated_at = ? WHERE order_id = ? AND deleted_at IS NULL',
|
|
21
|
+
[timestamp, timestamp, orderId]
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async insert(data) {
|
|
26
|
+
const timestamp = nowSql();
|
|
27
|
+
const id = data.id || uuid();
|
|
28
|
+
await this.db.insert('order_payments', {
|
|
29
|
+
id,
|
|
30
|
+
name: data.name ?? null,
|
|
31
|
+
order_id: data.order_id,
|
|
32
|
+
payment_method_id: data.payment_method_id,
|
|
33
|
+
amount: data.amount ?? 0,
|
|
34
|
+
real_amount: data.real_amount ?? 0,
|
|
35
|
+
change: data.change ?? 0,
|
|
36
|
+
created_at: timestamp,
|
|
37
|
+
updated_at: timestamp,
|
|
38
|
+
});
|
|
39
|
+
return id;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
module.exports = PaymentRepository;
|