@alizadefaig/pos-server 1.2.26 → 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 +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,176 @@
|
|
|
1
|
+
const {money, toFloat, toInt, toBool, sum} = require('../support/num');
|
|
2
|
+
|
|
3
|
+
const STATUS_CLOSED = 'closed';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Чек. Порт App\Models\Order — расчётная часть один в один.
|
|
7
|
+
* Персистентность и связи наполняет OrderRepository.
|
|
8
|
+
*/
|
|
9
|
+
class Order {
|
|
10
|
+
constructor(row = {}, {products = [], payments = [], source = null, table = null, hall = null, waiter = null} = {}) {
|
|
11
|
+
Object.assign(this, row);
|
|
12
|
+
|
|
13
|
+
this.products = products;
|
|
14
|
+
this.payments = payments;
|
|
15
|
+
this.source = source;
|
|
16
|
+
this.table = table;
|
|
17
|
+
this.hall = hall;
|
|
18
|
+
this.waiter = waiter;
|
|
19
|
+
|
|
20
|
+
this.customers = Order.parseCustomers(row.customers);
|
|
21
|
+
this.guest_count = toInt(row.guest_count, 0);
|
|
22
|
+
this.slot_index = toInt(row.slot_index, 1);
|
|
23
|
+
this.number = toInt(row.number, 1);
|
|
24
|
+
this.global_number = toInt(row.global_number, 1);
|
|
25
|
+
this.discount_type = row.discount_type || 'amount';
|
|
26
|
+
this.status = row.status || 'created';
|
|
27
|
+
this.is_closed_without_payment = toBool(row.is_closed_without_payment);
|
|
28
|
+
this.should_send_to_kitchen = toBool(row.should_send_to_kitchen);
|
|
29
|
+
this.service_charge = toFloat(row.service_charge, 0);
|
|
30
|
+
|
|
31
|
+
for (const field of [
|
|
32
|
+
'subtotal', 'total', 'profit', 'cost', 'discount', 'discount_amount',
|
|
33
|
+
'service_charge_amount', 'max_discount_amount', 'deposit_amount', 'deposit_gap',
|
|
34
|
+
'tax', 'tip',
|
|
35
|
+
]) {
|
|
36
|
+
this[field] = money(row[field] ?? 0);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Флаг из Laravel: apply*-методы уже пересчитали позиции, повторный проход в calculate()
|
|
40
|
+
// только удвоил бы сервисный сбор.
|
|
41
|
+
this.calculateWithProducts = true;
|
|
42
|
+
// service_charge_calculate_mode команды — проставляет сервис при загрузке чека.
|
|
43
|
+
this.serviceChargeMode = 'after_discount';
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
static parseCustomers(value) {
|
|
47
|
+
if (Array.isArray(value)) {
|
|
48
|
+
return value;
|
|
49
|
+
}
|
|
50
|
+
if (typeof value === 'string' && value.length) {
|
|
51
|
+
try {
|
|
52
|
+
const parsed = JSON.parse(value);
|
|
53
|
+
return Array.isArray(parsed) ? parsed : [1];
|
|
54
|
+
} catch (e) {
|
|
55
|
+
return [1];
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return [1];
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
isClosed() {
|
|
62
|
+
return this.status === STATUS_CLOSED;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
applyServiceCharge(serviceCharge) {
|
|
66
|
+
this.service_charge = toFloat(serviceCharge);
|
|
67
|
+
for (const product of this.products) {
|
|
68
|
+
product.applyServiceCharge(serviceCharge).calculate(this.serviceChargeMode);
|
|
69
|
+
}
|
|
70
|
+
this.calculateWithProducts = false;
|
|
71
|
+
return this;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
applyPercentDiscount(percent) {
|
|
75
|
+
this.discount = money(percent);
|
|
76
|
+
this.discount_type = 'percent';
|
|
77
|
+
for (const product of this.products) {
|
|
78
|
+
product.applyPercentDiscount(percent).calculate(this.serviceChargeMode);
|
|
79
|
+
}
|
|
80
|
+
this.calculateWithProducts = false;
|
|
81
|
+
return this;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
applyAmountDiscount(amount) {
|
|
85
|
+
amount = toFloat(amount);
|
|
86
|
+
const maxDiscountAmount = Math.max(this.max_discount_amount, sum(this.products, 'max_discount_amount'));
|
|
87
|
+
if (maxDiscountAmount < amount) {
|
|
88
|
+
return this;
|
|
89
|
+
}
|
|
90
|
+
this.discount = money(Math.max(0, Math.min(amount, maxDiscountAmount)));
|
|
91
|
+
this.discount_type = 'amount';
|
|
92
|
+
|
|
93
|
+
// Сумма скидки размазывается по позициям в порядке чека, пока не кончится.
|
|
94
|
+
let left = amount;
|
|
95
|
+
for (const product of this.products) {
|
|
96
|
+
if (left <= 0) {
|
|
97
|
+
product.applyAmountDiscount(0);
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
const productDiscountAmount = Math.min(product.max_discount_amount, left);
|
|
101
|
+
left -= productDiscountAmount;
|
|
102
|
+
product.applyAmountDiscount(productDiscountAmount).calculate(this.serviceChargeMode);
|
|
103
|
+
}
|
|
104
|
+
this.calculateWithProducts = false;
|
|
105
|
+
return this;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Депозит стола: если сумма подходящих позиций меньше депозита, разницу добираем
|
|
110
|
+
* отдельной строкой и облагаем тем же сервисным сбором.
|
|
111
|
+
*/
|
|
112
|
+
applyDepositGap() {
|
|
113
|
+
this.deposit_gap = 0;
|
|
114
|
+
|
|
115
|
+
const depositAmount = toFloat(this.deposit_amount);
|
|
116
|
+
if (depositAmount <= 0) {
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const eligibleSubtotal = this.products
|
|
121
|
+
.filter((product) => !product.productFlag('ignore_deposit'))
|
|
122
|
+
.reduce((carry, product) => carry + toFloat(product.subtotal), 0);
|
|
123
|
+
|
|
124
|
+
const gap = Math.max(0, depositAmount - eligibleSubtotal);
|
|
125
|
+
if (gap <= 0) {
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
this.deposit_gap = money(gap);
|
|
130
|
+
this.total = money(this.total + gap);
|
|
131
|
+
|
|
132
|
+
if (this.service_charge > 0) {
|
|
133
|
+
const gapServiceCharge = money(gap * this.service_charge / 100);
|
|
134
|
+
this.service_charge_amount = money(this.service_charge_amount + gapServiceCharge);
|
|
135
|
+
this.total = money(this.total + gapServiceCharge);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
calculate(withProducts = true, full = false) {
|
|
140
|
+
if ((withProducts && this.calculateWithProducts) || full) {
|
|
141
|
+
for (const product of this.products) {
|
|
142
|
+
if (!full || (this.service_charge == 0 && this.discount == 0)) {
|
|
143
|
+
product.calculate(this.serviceChargeMode);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
if (full) {
|
|
147
|
+
if (this.service_charge > 0) {
|
|
148
|
+
this.applyServiceCharge(this.service_charge);
|
|
149
|
+
}
|
|
150
|
+
if (this.discount > 0) {
|
|
151
|
+
if (this.discount_type === 'percent') {
|
|
152
|
+
this.applyPercentDiscount(this.discount);
|
|
153
|
+
} else {
|
|
154
|
+
this.applyAmountDiscount(this.discount);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
this.subtotal = money(sum(this.products, 'subtotal'));
|
|
161
|
+
this.profit = money(sum(this.products, 'profit'));
|
|
162
|
+
this.cost = money(sum(this.products, 'cost_total'));
|
|
163
|
+
this.service_charge_amount = money(sum(this.products, 'service_charge_amount'));
|
|
164
|
+
this.discount_amount = money(sum(this.products, 'discount_amount'));
|
|
165
|
+
this.max_discount_amount = money(sum(this.products, 'max_discount_amount'));
|
|
166
|
+
this.total = money(sum(this.products, 'total'));
|
|
167
|
+
|
|
168
|
+
this.applyDepositGap();
|
|
169
|
+
|
|
170
|
+
return this;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
Order.STATUS_CLOSED = STATUS_CLOSED;
|
|
175
|
+
|
|
176
|
+
module.exports = Order;
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
const {money, qty, toFloat, toInt, toBool} = require('../support/num');
|
|
2
|
+
const {now, parse, diffInSeconds} = require('../support/time');
|
|
3
|
+
|
|
4
|
+
const TYPE_PRODUCT = 'product';
|
|
5
|
+
const TYPE_TIMER = 'timer';
|
|
6
|
+
const TYPE_EXTRA = 'extra';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Позиция чека. Порт App\Models\OrderProduct — расчётная часть один в один,
|
|
10
|
+
* персистентность вынесена в OrderProductRepository (модель ничего не знает о БД).
|
|
11
|
+
*/
|
|
12
|
+
class OrderProduct {
|
|
13
|
+
constructor(row = {}, {product = null, modifiers = []} = {}) {
|
|
14
|
+
Object.assign(this, row);
|
|
15
|
+
|
|
16
|
+
this.product = product;
|
|
17
|
+
this.modifiers = modifiers;
|
|
18
|
+
|
|
19
|
+
// Не колонка БД: сколько минут «накрутил» таймер к моменту расчёта. Уезжает в ресурс
|
|
20
|
+
// и в чек, поэтому пересчитывается при каждом calculate().
|
|
21
|
+
this.elapsed_total_minutes = toInt(row.elapsed_total_minutes, 0);
|
|
22
|
+
|
|
23
|
+
this.quantity = qty(row.quantity ?? 1);
|
|
24
|
+
this.price = money(row.price ?? 0);
|
|
25
|
+
this.guest_number = toInt(row.guest_number, 1);
|
|
26
|
+
this.type = row.type || TYPE_PRODUCT;
|
|
27
|
+
this.discount_type = row.discount_type || 'percent';
|
|
28
|
+
this.is_stopped = toBool(row.is_stopped);
|
|
29
|
+
this.is_fixed = toBool(row.is_fixed);
|
|
30
|
+
this.is_gift = toBool(row.is_gift);
|
|
31
|
+
this.paused_seconds = toInt(row.paused_seconds, 0);
|
|
32
|
+
this.timer_limit_minutes = row.timer_limit_minutes ? toInt(row.timer_limit_minutes) : null;
|
|
33
|
+
|
|
34
|
+
for (const field of [
|
|
35
|
+
'cost', 'profit', 'cost_total', 'price_total', 'subtotal', 'total',
|
|
36
|
+
'price_after_service_charge', 'price_after_discount',
|
|
37
|
+
'discount', 'discount_amount', 'service_charge', 'service_charge_amount',
|
|
38
|
+
'max_discount_amount',
|
|
39
|
+
]) {
|
|
40
|
+
this[field] = money(row[field] ?? 0);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
for (const field of [
|
|
44
|
+
'print_quantity', 'receipt_print_quantity', 'workshop_print_quantity',
|
|
45
|
+
'kds_print_quantity', 'total_workshop_print_quantity',
|
|
46
|
+
]) {
|
|
47
|
+
this[field] = qty(row[field] ?? 0);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
productFlag(name, fallback = false) {
|
|
52
|
+
if (!this.product) {
|
|
53
|
+
return fallback;
|
|
54
|
+
}
|
|
55
|
+
return toBool(this.product[name]);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
applyServiceCharge(serviceCharge) {
|
|
59
|
+
if (this.productFlag('ignore_service_charge')) {
|
|
60
|
+
this.service_charge = 0;
|
|
61
|
+
return this;
|
|
62
|
+
}
|
|
63
|
+
this.service_charge = Math.min(Math.max(0, toFloat(serviceCharge)), 100);
|
|
64
|
+
return this;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
applyAmountDiscount(discount) {
|
|
68
|
+
this.discount_type = 'amount';
|
|
69
|
+
this.discount = 0;
|
|
70
|
+
|
|
71
|
+
if (toFloat(discount) > this.max_discount_amount) {
|
|
72
|
+
return this;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
this.discount = money(discount);
|
|
76
|
+
this.discount_amount = this.discount;
|
|
77
|
+
return this;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
applyPercentDiscount(discount) {
|
|
81
|
+
if (this.productFlag('is_not_discount')) {
|
|
82
|
+
this.discount = 0;
|
|
83
|
+
return this;
|
|
84
|
+
}
|
|
85
|
+
if (toFloat(discount) > 90 && !this.productFlag('is_giftable')) {
|
|
86
|
+
this.discount = 0;
|
|
87
|
+
return this;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
this.discount_type = 'percent';
|
|
91
|
+
this.discount = money(discount);
|
|
92
|
+
return this;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* @param {string} mode service_charge_calculate_mode команды: before_discount | after_discount
|
|
97
|
+
*/
|
|
98
|
+
calculate(mode = 'after_discount') {
|
|
99
|
+
mode = mode || 'after_discount';
|
|
100
|
+
|
|
101
|
+
if (!this.cost) {
|
|
102
|
+
this.cost = money(this.product ? this.product.cost : 0);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Цена, от которой пляшут сбор и скидка. У обычной позиции в неё входят модификаторы,
|
|
106
|
+
// у повременной — нет (там цена целиком выводится из минут).
|
|
107
|
+
let price = this.price;
|
|
108
|
+
let cost = this.cost;
|
|
109
|
+
|
|
110
|
+
if (this.type === TYPE_TIMER) {
|
|
111
|
+
cost = this.calculateTimer(price);
|
|
112
|
+
} else {
|
|
113
|
+
const modifiersTotal = this.modifiers.reduce((carry, modifier) => carry + toFloat(modifier.total), 0);
|
|
114
|
+
if (modifiersTotal > 0) {
|
|
115
|
+
price += modifiersTotal;
|
|
116
|
+
}
|
|
117
|
+
this.price_total = money(price * this.quantity);
|
|
118
|
+
this.subtotal = money(price * this.quantity);
|
|
119
|
+
this.total = money(price * this.quantity);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
this.calculateBase(price, mode);
|
|
123
|
+
|
|
124
|
+
this.cost_total = money(cost * this.quantity);
|
|
125
|
+
this.profit = money(this.total - this.cost_total);
|
|
126
|
+
return this;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** Общая для обеих веток часть: максимум скидки, сервисный сбор и скидка в нужном порядке. */
|
|
130
|
+
calculateBase(price, mode) {
|
|
131
|
+
this.max_discount_amount = money(this.subtotal * 90 / 100);
|
|
132
|
+
if (this.productFlag('is_not_discount')) {
|
|
133
|
+
this.max_discount_amount = 0;
|
|
134
|
+
} else if (this.productFlag('is_giftable')) {
|
|
135
|
+
this.max_discount_amount = this.subtotal;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const isTimer = this.type === TYPE_TIMER;
|
|
139
|
+
if (mode === 'before_discount') {
|
|
140
|
+
if (!isTimer) {
|
|
141
|
+
this.calculateServiceCharge(price);
|
|
142
|
+
} else {
|
|
143
|
+
this.price_after_service_charge = money(price);
|
|
144
|
+
}
|
|
145
|
+
this.calculateDiscount(this.price_after_service_charge);
|
|
146
|
+
} else {
|
|
147
|
+
this.calculateDiscount(price);
|
|
148
|
+
if (!isTimer) {
|
|
149
|
+
this.calculateServiceCharge(this.price_after_discount);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Повременная позиция: цена считается от прошедших минут, а не от количества.
|
|
156
|
+
* grace (after_how_many_apply) действует только в почасовом режиме — как в Laravel.
|
|
157
|
+
*/
|
|
158
|
+
calculateTimer(price) {
|
|
159
|
+
const data = this.productData();
|
|
160
|
+
const timerData = (data && data.timer) || {};
|
|
161
|
+
|
|
162
|
+
let reference = now();
|
|
163
|
+
if (this.is_stopped && this.stopped_at) {
|
|
164
|
+
reference = parse(this.stopped_at);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const pausedSeconds = this.paused_seconds || 0;
|
|
168
|
+
const diffSeconds = Math.max(0, diffInSeconds(this.added_at, reference) - pausedSeconds);
|
|
169
|
+
let diffMinutes = Math.ceil(diffSeconds / 60);
|
|
170
|
+
if (this.timer_limit_minutes && diffMinutes > this.timer_limit_minutes) {
|
|
171
|
+
diffMinutes = this.timer_limit_minutes;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const grace = toFloat(timerData.after_how_many_apply);
|
|
175
|
+
const interval = 60;
|
|
176
|
+
const isPerMinute = toBool(timerData.is_per_minute);
|
|
177
|
+
const firstPrice = price;
|
|
178
|
+
const secondPrice = toFloat(timerData.after_price);
|
|
179
|
+
const thresholdMinutes = toBool(timerData.is_adjustable) ? toInt(timerData.after_minutes) : null;
|
|
180
|
+
|
|
181
|
+
const calcCost = (minutes, rate) => {
|
|
182
|
+
if (isPerMinute) {
|
|
183
|
+
return minutes * (rate / interval);
|
|
184
|
+
}
|
|
185
|
+
// Почасовой блок: первые 60 минут — целый час, дальше поминутно.
|
|
186
|
+
if (minutes <= interval) {
|
|
187
|
+
return rate;
|
|
188
|
+
}
|
|
189
|
+
return rate + ((minutes - interval) * (rate / interval));
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
let cost;
|
|
193
|
+
if (!isPerMinute && diffMinutes <= grace) {
|
|
194
|
+
cost = 0;
|
|
195
|
+
} else if (thresholdMinutes === null || diffMinutes <= thresholdMinutes) {
|
|
196
|
+
cost = calcCost(diffMinutes, firstPrice);
|
|
197
|
+
} else {
|
|
198
|
+
cost = calcCost(thresholdMinutes, firstPrice) + calcCost(diffMinutes - thresholdMinutes, secondPrice);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
this.elapsed_total_minutes = toInt(diffMinutes);
|
|
202
|
+
this.price_total = money(cost);
|
|
203
|
+
this.subtotal = money(cost);
|
|
204
|
+
this.total = money(cost);
|
|
205
|
+
|
|
206
|
+
return cost;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
productData() {
|
|
210
|
+
if (!this.product) {
|
|
211
|
+
return {};
|
|
212
|
+
}
|
|
213
|
+
if (typeof this.product.data === 'string') {
|
|
214
|
+
try {
|
|
215
|
+
return JSON.parse(this.product.data) || {};
|
|
216
|
+
} catch (e) {
|
|
217
|
+
return {};
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
return this.product.data || {};
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
calculateDiscount(price) {
|
|
224
|
+
if (this.discount_type === 'percent') {
|
|
225
|
+
const discountPerItem = Math.max(price * (this.discount / 100), 0);
|
|
226
|
+
this.price_after_discount = money(Math.max(price - discountPerItem, 0));
|
|
227
|
+
this.discount_amount = money(Math.max(discountPerItem * this.quantity, 0));
|
|
228
|
+
this.total = money(Math.max(this.total - this.discount_amount, 0));
|
|
229
|
+
} else {
|
|
230
|
+
const discountPerItem = this.quantity === 0 ? 0 : this.discount / this.quantity;
|
|
231
|
+
this.price_after_discount = money(price - discountPerItem);
|
|
232
|
+
this.discount_amount = this.discount;
|
|
233
|
+
this.total = money(this.total - this.discount_amount);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
calculateServiceCharge(price) {
|
|
238
|
+
const serviceChargePerItem = Math.max(price * (this.service_charge / 100), 0);
|
|
239
|
+
this.price_after_service_charge = money(price + serviceChargePerItem);
|
|
240
|
+
this.service_charge_amount = money(this.quantity * serviceChargePerItem);
|
|
241
|
+
this.total = money(this.total + this.service_charge_amount);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
OrderProduct.TYPE_PRODUCT = TYPE_PRODUCT;
|
|
246
|
+
OrderProduct.TYPE_TIMER = TYPE_TIMER;
|
|
247
|
+
OrderProduct.TYPE_EXTRA = TYPE_EXTRA;
|
|
248
|
+
|
|
249
|
+
module.exports = OrderProduct;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
const {nowSql, toSql} = require('../support/time');
|
|
2
|
+
const {phpJsonEncode} = require('../support/str');
|
|
3
|
+
const {uuid} = require('../support/ids');
|
|
4
|
+
|
|
5
|
+
// Ровно $fillable модели OrderEvent. В Laravel лишние ключи события (price, user_id)
|
|
6
|
+
// молча отбрасываются массовым заполнением — здесь их отбрасывает этот же список.
|
|
7
|
+
const COLUMNS = [
|
|
8
|
+
'platform_event_id', 'event', 'team_user_id', 'team_id', 'order_id', 'terminal_id',
|
|
9
|
+
'accepted_by', 'product_id', 'quantity', 'data', 'time_at', 'ip_address',
|
|
10
|
+
];
|
|
11
|
+
|
|
12
|
+
class EventRepository {
|
|
13
|
+
constructor(db) {
|
|
14
|
+
this.db = db;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Порт OrderService::saveEvents (запись). Уведомления в Telegram шлёт OrderService.saveEvents
|
|
19
|
+
* через TelegramService — только в облаке; с кассы событие уедет наверх push'ем.
|
|
20
|
+
*/
|
|
21
|
+
async saveEvents(events, context) {
|
|
22
|
+
const timestamp = nowSql();
|
|
23
|
+
for (const event of events) {
|
|
24
|
+
const payload = {
|
|
25
|
+
id: uuid(),
|
|
26
|
+
team_id: context.teamId,
|
|
27
|
+
team_user_id: context.employeeId,
|
|
28
|
+
terminal_id: context.terminalId,
|
|
29
|
+
ip_address: context.ipAddress,
|
|
30
|
+
time_at: toSql(event.time) || timestamp,
|
|
31
|
+
created_at: timestamp,
|
|
32
|
+
updated_at: timestamp,
|
|
33
|
+
};
|
|
34
|
+
for (const column of COLUMNS) {
|
|
35
|
+
if (event[column] === undefined) {
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
if (column === 'data') {
|
|
39
|
+
payload.data = event.data === null ? null : phpJsonEncode(event.data);
|
|
40
|
+
} else if (column === 'time_at') {
|
|
41
|
+
payload.time_at = toSql(event.time_at) || payload.time_at;
|
|
42
|
+
} else {
|
|
43
|
+
payload[column] = event[column];
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
await this.db.insert('order_events', payload);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
module.exports = EventRepository;
|