@darkpos/pricing 1.0.145 → 1.0.147
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/__TEST__/invoice/refundInvoices.test.js +239 -0
- package/__TEST__/order/addItem.test.js +69 -0
- package/__TEST__/order/pickEndDate.test.js +208 -2
- package/lib/constants/Store.js +3 -0
- package/lib/constants/index.js +2 -0
- package/lib/invoice/applyRefundToInvoices.js +52 -0
- package/lib/invoice/buildItemRefund.js +23 -0
- package/lib/invoice/computeRefundables.js +59 -0
- package/lib/invoice/index.js +8 -0
- package/lib/invoice/resolveItemState.js +25 -0
- package/lib/order/addItem.js +2 -0
- package/lib/store/getClosedDays.js +7 -0
- package/lib/store/getReadyDateCapacityDays.js +4 -0
- package/lib/store/getReadyDateCountsByDate.js +33 -0
- package/lib/store/getSchedule.js +61 -0
- package/lib/store/getStoreTimezone.js +7 -0
- package/lib/store/hasReadyDateCapacitySettings.js +8 -0
- package/lib/store/index.js +16 -0
- package/lib/store/normalizeLimit.js +5 -0
- package/lib/store/pickBaseEndDate.js +71 -0
- package/lib/store/pickEndDate.js +69 -95
- package/package.json +2 -2
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
const usePricing = require('../../index');
|
|
2
|
+
const mockStores = require('../mocks/stores');
|
|
3
|
+
|
|
4
|
+
const pricing = usePricing({ store: mockStores[0] });
|
|
5
|
+
|
|
6
|
+
const ORDER_ID = 'order-1';
|
|
7
|
+
const OVERPAID_ITEM_ID = 'item-0';
|
|
8
|
+
const UNDERPAID_ITEM_ID = 'item-1';
|
|
9
|
+
|
|
10
|
+
const liveOrder = {
|
|
11
|
+
_id: ORDER_ID,
|
|
12
|
+
items: [
|
|
13
|
+
{
|
|
14
|
+
_id: OVERPAID_ITEM_ID,
|
|
15
|
+
total: 10.5,
|
|
16
|
+
totalPaid: 14.7,
|
|
17
|
+
status: { paid: { value: true }, picked: { value: false } },
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
_id: UNDERPAID_ITEM_ID,
|
|
21
|
+
total: 10.5,
|
|
22
|
+
totalPaid: 10.3,
|
|
23
|
+
status: { paid: { value: false }, picked: { value: false } },
|
|
24
|
+
},
|
|
25
|
+
],
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const ordersById = { [ORDER_ID]: liveOrder };
|
|
29
|
+
|
|
30
|
+
const overpaidInvoiceItem = {
|
|
31
|
+
orderId: ORDER_ID,
|
|
32
|
+
orderItemId: OVERPAID_ITEM_ID,
|
|
33
|
+
total: 14.7,
|
|
34
|
+
totalPaid: 14.7,
|
|
35
|
+
amount: 14.7,
|
|
36
|
+
status: { paid: { value: true }, picked: { value: false } },
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const partiallyPaidInvoiceItem = {
|
|
40
|
+
orderId: ORDER_ID,
|
|
41
|
+
orderItemId: UNDERPAID_ITEM_ID,
|
|
42
|
+
total: 13.65,
|
|
43
|
+
totalPaid: 10.3,
|
|
44
|
+
amount: 10.3,
|
|
45
|
+
status: { paid: { value: false }, picked: { value: false } },
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
describe('computeRefundables', () => {
|
|
49
|
+
const overpaidState = pricing.invoice.resolveItemState(
|
|
50
|
+
overpaidInvoiceItem,
|
|
51
|
+
ordersById
|
|
52
|
+
); // excess +4.2
|
|
53
|
+
const underpaidState = pricing.invoice.resolveItemState(
|
|
54
|
+
partiallyPaidInvoiceItem,
|
|
55
|
+
ordersById
|
|
56
|
+
); // excess -0.2
|
|
57
|
+
|
|
58
|
+
test('refunds the full excess from the overpaid item and redistributes the surplus to the underpaid item', () => {
|
|
59
|
+
const [overpaidRefundable, underpaidCredit] =
|
|
60
|
+
pricing.invoice.computeRefundables([overpaidState, underpaidState], 4);
|
|
61
|
+
expect(overpaidRefundable).toBe(4.2); // full overpayment cleared
|
|
62
|
+
expect(underpaidCredit).toBe(-0.2); // 0.2 credit applied to cover the gap
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
test('net refund amount across all items equals the requested refund total', () => {
|
|
66
|
+
const [overpaidRefundable, underpaidCredit] =
|
|
67
|
+
pricing.invoice.computeRefundables([overpaidState, underpaidState], 4);
|
|
68
|
+
expect(overpaidRefundable + underpaidCredit).toBe(4);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
test('result is order-independent — same outcome when items are reversed', () => {
|
|
72
|
+
const [underpaidCredit, overpaidRefundable] =
|
|
73
|
+
pricing.invoice.computeRefundables([underpaidState, overpaidState], 4);
|
|
74
|
+
expect(overpaidRefundable).toBe(4.2);
|
|
75
|
+
expect(underpaidCredit).toBe(-0.2);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
test('never-paid item receives no credit even when overpaid surplus exists', () => {
|
|
79
|
+
const neverPaidState = {
|
|
80
|
+
...pricing.invoice.resolveItemState(partiallyPaidInvoiceItem, ordersById),
|
|
81
|
+
itemTotalPaid: 0,
|
|
82
|
+
excess: -10.5,
|
|
83
|
+
};
|
|
84
|
+
const [overpaidRefundable, neverPaidRefundable] =
|
|
85
|
+
pricing.invoice.computeRefundables([overpaidState, neverPaidState], 4);
|
|
86
|
+
expect(overpaidRefundable).toBe(4); // capped — no surplus to redistribute
|
|
87
|
+
expect(neverPaidRefundable).toBe(0); // never-paid item stays untouched
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
test('draws directly from totalPaid when no overpaid items exist', () => {
|
|
91
|
+
const exactlyPaidState = { ...overpaidState, excess: 0 };
|
|
92
|
+
const [refundable] = pricing.invoice.computeRefundables(
|
|
93
|
+
[exactlyPaidState],
|
|
94
|
+
5
|
|
95
|
+
);
|
|
96
|
+
expect(refundable).toBe(5);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
test('returns all zeros when the requested refund amount is 0', () => {
|
|
100
|
+
const refundables = pricing.invoice.computeRefundables(
|
|
101
|
+
[overpaidState, underpaidState],
|
|
102
|
+
0
|
|
103
|
+
);
|
|
104
|
+
expect(refundables).toEqual([0, 0]);
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
describe('applyRefundToInvoices', () => {
|
|
109
|
+
const invoicesForOnePayment = [
|
|
110
|
+
{
|
|
111
|
+
_id: 'inv-1',
|
|
112
|
+
amount: 25,
|
|
113
|
+
paymentOrderItems: [overpaidInvoiceItem, partiallyPaidInvoiceItem],
|
|
114
|
+
},
|
|
115
|
+
];
|
|
116
|
+
|
|
117
|
+
test('the refund invoice has the correct total amount and both item refunds', () => {
|
|
118
|
+
const refundInvoices = pricing.invoice.applyRefundToInvoices(
|
|
119
|
+
invoicesForOnePayment,
|
|
120
|
+
ordersById,
|
|
121
|
+
4
|
|
122
|
+
);
|
|
123
|
+
expect(refundInvoices).toHaveLength(1);
|
|
124
|
+
expect(refundInvoices[0].amount).toBe(-4);
|
|
125
|
+
expect(refundInvoices[0].paymentOrderItems).toHaveLength(2);
|
|
126
|
+
expect(refundInvoices[0].paymentOrderItems[0].amount).toBe(-4.2);
|
|
127
|
+
expect(refundInvoices[0].paymentOrderItems[1].amount).toBe(0.2);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
test('the overpaid item is cleared to its exact total and marked as paid', () => {
|
|
131
|
+
const refundInvoices = pricing.invoice.applyRefundToInvoices(
|
|
132
|
+
invoicesForOnePayment,
|
|
133
|
+
ordersById,
|
|
134
|
+
4
|
|
135
|
+
);
|
|
136
|
+
expect(refundInvoices[0].paymentOrderItems[0].status.paid.value).toBe(true);
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
test('the underpaid item receives a credit that covers its remaining balance and is marked as paid', () => {
|
|
140
|
+
const refundInvoices = pricing.invoice.applyRefundToInvoices(
|
|
141
|
+
invoicesForOnePayment,
|
|
142
|
+
ordersById,
|
|
143
|
+
4
|
|
144
|
+
);
|
|
145
|
+
expect(refundInvoices[0].paymentOrderItems[1].status.paid.value).toBe(true);
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
test('returns an empty array when the refund amount is 0', () => {
|
|
149
|
+
const refundInvoices = pricing.invoice.applyRefundToInvoices(
|
|
150
|
+
invoicesForOnePayment,
|
|
151
|
+
ordersById,
|
|
152
|
+
0
|
|
153
|
+
);
|
|
154
|
+
expect(refundInvoices).toEqual([]);
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
test('skips invoices that are already refunded or have no positive balance', () => {
|
|
158
|
+
const invoicesIncludingAlreadyRefunded = [
|
|
159
|
+
{
|
|
160
|
+
_id: 'inv-already-refunded',
|
|
161
|
+
amount: -5,
|
|
162
|
+
paymentOrderItems: [overpaidInvoiceItem],
|
|
163
|
+
},
|
|
164
|
+
...invoicesForOnePayment,
|
|
165
|
+
];
|
|
166
|
+
const refundInvoices = pricing.invoice.applyRefundToInvoices(
|
|
167
|
+
invoicesIncludingAlreadyRefunded,
|
|
168
|
+
ordersById,
|
|
169
|
+
4
|
|
170
|
+
);
|
|
171
|
+
expect(refundInvoices).toHaveLength(1);
|
|
172
|
+
expect(refundInvoices[0]._id).toBe('inv-1');
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
test('distributes the refund across multiple invoices when the first is exhausted', () => {
|
|
176
|
+
const splitPaymentInvoices = [
|
|
177
|
+
{
|
|
178
|
+
_id: 'first-payment',
|
|
179
|
+
amount: 14.7,
|
|
180
|
+
paymentOrderItems: [overpaidInvoiceItem],
|
|
181
|
+
},
|
|
182
|
+
{
|
|
183
|
+
_id: 'second-payment',
|
|
184
|
+
amount: 10.3,
|
|
185
|
+
paymentOrderItems: [partiallyPaidInvoiceItem],
|
|
186
|
+
},
|
|
187
|
+
];
|
|
188
|
+
// Refund 5: overpaid item covers 4.2, the remaining 0.8 comes from the second invoice
|
|
189
|
+
const refundInvoices = pricing.invoice.applyRefundToInvoices(
|
|
190
|
+
splitPaymentInvoices,
|
|
191
|
+
ordersById,
|
|
192
|
+
5
|
|
193
|
+
);
|
|
194
|
+
expect(refundInvoices).toHaveLength(2);
|
|
195
|
+
expect(refundInvoices[0].amount).toBe(-4.2);
|
|
196
|
+
expect(refundInvoices[1].amount).toBe(-0.8);
|
|
197
|
+
});
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
describe('buildItemRefund', () => {
|
|
201
|
+
const overpaidItemState = pricing.invoice.resolveItemState(
|
|
202
|
+
overpaidInvoiceItem,
|
|
203
|
+
ordersById
|
|
204
|
+
);
|
|
205
|
+
const underpaidItemState = pricing.invoice.resolveItemState(
|
|
206
|
+
partiallyPaidInvoiceItem,
|
|
207
|
+
ordersById
|
|
208
|
+
);
|
|
209
|
+
|
|
210
|
+
test('overpaid item: refunding its full excess results in a negative amount and paid = true', () => {
|
|
211
|
+
const itemRefund = pricing.invoice.buildItemRefund(overpaidItemState, 4.2);
|
|
212
|
+
expect(itemRefund.amount).toBe(-4.2);
|
|
213
|
+
expect(itemRefund.totalPaid).toBe(14.7); // pre-refund value preserved
|
|
214
|
+
expect(itemRefund.total).toBe(10.5);
|
|
215
|
+
expect(itemRefund.status.paid.value).toBe(true); // 14.7 - 4.2 = 10.5 >= 10.5
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
test('underpaid item: applying a credit results in a positive amount and paid = true', () => {
|
|
219
|
+
const itemRefund = pricing.invoice.buildItemRefund(
|
|
220
|
+
underpaidItemState,
|
|
221
|
+
-0.2
|
|
222
|
+
);
|
|
223
|
+
expect(itemRefund.amount).toBe(0.2); // positive = credit
|
|
224
|
+
expect(itemRefund.totalPaid).toBe(10.3);
|
|
225
|
+
expect(itemRefund.total).toBe(10.5);
|
|
226
|
+
expect(itemRefund.status.paid.value).toBe(true); // 10.3 - (-0.2) = 10.5 >= 10.5
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
test('paid stays false when the refund does not fully cover the remaining balance', () => {
|
|
230
|
+
const itemRefund = pricing.invoice.buildItemRefund(underpaidItemState, 2); // totalPaid drops to 8.3
|
|
231
|
+
expect(itemRefund.status.paid.value).toBe(false); // 8.3 < 10.5
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
test('preserves all other fields from the original invoice item', () => {
|
|
235
|
+
const itemRefund = pricing.invoice.buildItemRefund(overpaidItemState, 4.2);
|
|
236
|
+
expect(itemRefund.orderId).toBe(ORDER_ID);
|
|
237
|
+
expect(itemRefund.orderItemId).toBe(OVERPAID_ITEM_ID);
|
|
238
|
+
});
|
|
239
|
+
});
|
|
@@ -201,6 +201,75 @@ describe('addItem function', () => {
|
|
|
201
201
|
expect(updatedOrder.items[0].modifiers).toHaveLength(2); // Added item should have modifiers
|
|
202
202
|
});
|
|
203
203
|
|
|
204
|
+
test('should return original order when item serial is duplicated', () => {
|
|
205
|
+
const { addItem } = pricingService.order;
|
|
206
|
+
|
|
207
|
+
const order = {
|
|
208
|
+
items: [
|
|
209
|
+
{
|
|
210
|
+
_id: 'existing-item',
|
|
211
|
+
itemId: '111',
|
|
212
|
+
quantity: 1,
|
|
213
|
+
price: 100,
|
|
214
|
+
serial: 'ABC-123',
|
|
215
|
+
modifiers: [],
|
|
216
|
+
},
|
|
217
|
+
],
|
|
218
|
+
};
|
|
219
|
+
|
|
220
|
+
const item = {
|
|
221
|
+
_id: 'new-item',
|
|
222
|
+
itemId: '222',
|
|
223
|
+
quantity: 1,
|
|
224
|
+
price: 50,
|
|
225
|
+
serial: ' abc-123 ',
|
|
226
|
+
modifiers: [],
|
|
227
|
+
};
|
|
228
|
+
|
|
229
|
+
const result = addItem({
|
|
230
|
+
order,
|
|
231
|
+
item,
|
|
232
|
+
itemIndex: -1,
|
|
233
|
+
overridenQuantity: -1,
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
expect(result).toBe(order);
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
test('should return original order when autoBarcode serial is duplicated', () => {
|
|
240
|
+
const { addItem } = pricingService.order;
|
|
241
|
+
|
|
242
|
+
const order = {
|
|
243
|
+
items: [
|
|
244
|
+
{
|
|
245
|
+
_id: 'existing-item',
|
|
246
|
+
itemId: '111',
|
|
247
|
+
quantity: 1,
|
|
248
|
+
price: 100,
|
|
249
|
+
serial: 'ABC-123',
|
|
250
|
+
modifiers: [],
|
|
251
|
+
},
|
|
252
|
+
],
|
|
253
|
+
};
|
|
254
|
+
|
|
255
|
+
const item = {
|
|
256
|
+
_id: 'new-item',
|
|
257
|
+
itemId: '222',
|
|
258
|
+
quantity: 1,
|
|
259
|
+
price: 50,
|
|
260
|
+
modifiers: [],
|
|
261
|
+
};
|
|
262
|
+
|
|
263
|
+
const result = addItem({
|
|
264
|
+
order,
|
|
265
|
+
item,
|
|
266
|
+
itemIndex: 1,
|
|
267
|
+
autoBarcode: 'ABC-123',
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
expect(result).toBe(order);
|
|
271
|
+
});
|
|
272
|
+
|
|
204
273
|
test('Add Related Item', () => {
|
|
205
274
|
const { addItem } = pricingService.order;
|
|
206
275
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
const moment = require('moment-timezone');
|
|
2
2
|
const usePricing = require('../../lib/index');
|
|
3
3
|
|
|
4
|
-
const getDefaultSettings = schedules => ({
|
|
4
|
+
const getDefaultSettings = (schedules, readyDateCapacityDays = []) => ({
|
|
5
5
|
store: {
|
|
6
6
|
_settings: {
|
|
7
7
|
schedule: {
|
|
@@ -24,6 +24,9 @@ const getDefaultSettings = schedules => ({
|
|
|
24
24
|
cutHour: null,
|
|
25
25
|
},
|
|
26
26
|
],
|
|
27
|
+
readyDateCapacity: {
|
|
28
|
+
days: readyDateCapacityDays,
|
|
29
|
+
},
|
|
27
30
|
},
|
|
28
31
|
},
|
|
29
32
|
},
|
|
@@ -115,7 +118,7 @@ describe('pickEndDate function', () => {
|
|
|
115
118
|
recommended: 'nothing_recommended',
|
|
116
119
|
};
|
|
117
120
|
|
|
118
|
-
const now = moment().tz('America/New_York');
|
|
121
|
+
const now = moment('2024-08-25T15:00:00Z').tz('America/New_York');
|
|
119
122
|
|
|
120
123
|
jest.spyOn(moment, 'now').mockImplementation(() => now.valueOf());
|
|
121
124
|
const pricingServiceTest = usePricing(getDefaultSettings([schedule]));
|
|
@@ -299,4 +302,207 @@ describe('pickEndDate function', () => {
|
|
|
299
302
|
|
|
300
303
|
expect(result).toBe('2025-05-15T22:00:00Z');
|
|
301
304
|
});
|
|
305
|
+
|
|
306
|
+
test('pickEndDate - skips day when maxOrders capacity is reached', () => {
|
|
307
|
+
const schedule = {
|
|
308
|
+
addDays: 0,
|
|
309
|
+
readyHour: { hour: 17, minute: 0 },
|
|
310
|
+
skipDays: [],
|
|
311
|
+
cutHour: { hour: 16, minute: 0 },
|
|
312
|
+
cutDay: 1,
|
|
313
|
+
};
|
|
314
|
+
|
|
315
|
+
const pricingService = usePricing(
|
|
316
|
+
getDefaultSettings([schedule], [{ day: 2, maxOrders: 1 }])
|
|
317
|
+
);
|
|
318
|
+
|
|
319
|
+
const now = moment('2025-05-13T15:00:00Z').tz('America/New_York');
|
|
320
|
+
jest.spyOn(moment, 'now').mockImplementation(() => now.valueOf());
|
|
321
|
+
|
|
322
|
+
const result = pricingService.store.pickEndDate(
|
|
323
|
+
undefined,
|
|
324
|
+
undefined,
|
|
325
|
+
undefined,
|
|
326
|
+
[
|
|
327
|
+
{
|
|
328
|
+
date: '2025-05-13',
|
|
329
|
+
totalOrders: 1,
|
|
330
|
+
totalItems: 0,
|
|
331
|
+
},
|
|
332
|
+
]
|
|
333
|
+
);
|
|
334
|
+
|
|
335
|
+
expect(result).toBe('2025-05-14T21:00:00Z');
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
test('pickEndDate - skips day when maxItems capacity is reached', () => {
|
|
339
|
+
const schedule = {
|
|
340
|
+
addDays: 0,
|
|
341
|
+
readyHour: { hour: 17, minute: 0 },
|
|
342
|
+
skipDays: [],
|
|
343
|
+
cutHour: { hour: 16, minute: 0 },
|
|
344
|
+
cutDay: 1,
|
|
345
|
+
};
|
|
346
|
+
|
|
347
|
+
const pricingService = usePricing(
|
|
348
|
+
getDefaultSettings([schedule], [{ day: 2, maxItems: 1 }])
|
|
349
|
+
);
|
|
350
|
+
|
|
351
|
+
const now = moment('2025-05-13T15:00:00Z').tz('America/New_York');
|
|
352
|
+
jest.spyOn(moment, 'now').mockImplementation(() => now.valueOf());
|
|
353
|
+
|
|
354
|
+
const result = pricingService.store.pickEndDate(
|
|
355
|
+
undefined,
|
|
356
|
+
undefined,
|
|
357
|
+
undefined,
|
|
358
|
+
[
|
|
359
|
+
{
|
|
360
|
+
date: '2025-05-13',
|
|
361
|
+
totalOrders: 0,
|
|
362
|
+
totalItems: 1,
|
|
363
|
+
},
|
|
364
|
+
]
|
|
365
|
+
);
|
|
366
|
+
|
|
367
|
+
expect(result).toBe('2025-05-14T21:00:00Z');
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
test('pickEndDate - moves to next available day when capacity is full', () => {
|
|
371
|
+
const schedule = {
|
|
372
|
+
addDays: 2,
|
|
373
|
+
readyHour: { hour: 17, minute: 0 },
|
|
374
|
+
skipDays: [],
|
|
375
|
+
cutHour: { hour: 16, minute: 0 },
|
|
376
|
+
cutDay: 1,
|
|
377
|
+
dayOfWeek: [1, 2, 3, 4, 5],
|
|
378
|
+
};
|
|
379
|
+
|
|
380
|
+
const pricingService = usePricing(
|
|
381
|
+
getDefaultSettings([schedule], [{ day: 6, maxOrders: 1 }])
|
|
382
|
+
);
|
|
383
|
+
|
|
384
|
+
const now = moment('2026-02-26T15:00:00Z').tz('America/New_York');
|
|
385
|
+
jest.spyOn(moment, 'now').mockImplementation(() => now.valueOf());
|
|
386
|
+
|
|
387
|
+
const result = pricingService.store.pickEndDate(
|
|
388
|
+
undefined,
|
|
389
|
+
undefined,
|
|
390
|
+
undefined,
|
|
391
|
+
[
|
|
392
|
+
{
|
|
393
|
+
date: '2026-02-28',
|
|
394
|
+
totalOrders: 1,
|
|
395
|
+
totalItems: 0,
|
|
396
|
+
},
|
|
397
|
+
]
|
|
398
|
+
);
|
|
399
|
+
|
|
400
|
+
expect(result).toBe('2026-03-01T22:00:00Z');
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
test('pickEndDate - skips closed day after capacity overflow', () => {
|
|
404
|
+
const schedule = {
|
|
405
|
+
addDays: 2,
|
|
406
|
+
readyHour: { hour: 17, minute: 0 },
|
|
407
|
+
skipDays: null,
|
|
408
|
+
cutHour: { hour: 10, minute: 0 },
|
|
409
|
+
cutDay: 1,
|
|
410
|
+
};
|
|
411
|
+
|
|
412
|
+
const pricingService = usePricing({
|
|
413
|
+
store: {
|
|
414
|
+
_settings: {
|
|
415
|
+
schedule: {
|
|
416
|
+
closed: [{ date: '2026-03-01' }],
|
|
417
|
+
},
|
|
418
|
+
order: {
|
|
419
|
+
schedules: [schedule],
|
|
420
|
+
readyDateCapacity: {
|
|
421
|
+
days: [
|
|
422
|
+
{ day: 6, maxOrders: 2, maxItems: 10 },
|
|
423
|
+
{ day: 1, maxOrders: 10, maxItems: 10 },
|
|
424
|
+
],
|
|
425
|
+
},
|
|
426
|
+
},
|
|
427
|
+
},
|
|
428
|
+
},
|
|
429
|
+
});
|
|
430
|
+
|
|
431
|
+
const now = moment('2026-02-25T16:04:51Z').tz('America/New_York');
|
|
432
|
+
jest.spyOn(moment, 'now').mockImplementation(() => now.valueOf());
|
|
433
|
+
|
|
434
|
+
const result = pricingService.store.pickEndDate(
|
|
435
|
+
undefined,
|
|
436
|
+
undefined,
|
|
437
|
+
undefined,
|
|
438
|
+
[
|
|
439
|
+
{ date: '2026-02-26', totalOrders: 4, totalItems: 5 },
|
|
440
|
+
{ date: '2026-02-27', totalOrders: 1, totalItems: 1 },
|
|
441
|
+
{ date: '2026-02-28', totalOrders: 2, totalItems: 2 },
|
|
442
|
+
]
|
|
443
|
+
);
|
|
444
|
+
|
|
445
|
+
expect(result).toBe('2026-03-02T22:00:00Z');
|
|
446
|
+
});
|
|
447
|
+
|
|
448
|
+
test('pickEndDate - skips multiple consecutive capacity-full days', () => {
|
|
449
|
+
const schedule = {
|
|
450
|
+
addDays: 0,
|
|
451
|
+
readyHour: { hour: 17, minute: 0 },
|
|
452
|
+
skipDays: [],
|
|
453
|
+
cutHour: { hour: 16, minute: 0 },
|
|
454
|
+
cutDay: 1,
|
|
455
|
+
};
|
|
456
|
+
|
|
457
|
+
const pricingService = usePricing(
|
|
458
|
+
getDefaultSettings(
|
|
459
|
+
[schedule],
|
|
460
|
+
[
|
|
461
|
+
{ day: 2, maxOrders: 1 },
|
|
462
|
+
{ day: 3, maxOrders: 1 },
|
|
463
|
+
]
|
|
464
|
+
)
|
|
465
|
+
);
|
|
466
|
+
|
|
467
|
+
const now = moment('2025-05-13T15:00:00Z').tz('America/New_York');
|
|
468
|
+
jest.spyOn(moment, 'now').mockImplementation(() => now.valueOf());
|
|
469
|
+
|
|
470
|
+
const result = pricingService.store.pickEndDate(
|
|
471
|
+
undefined,
|
|
472
|
+
undefined,
|
|
473
|
+
undefined,
|
|
474
|
+
[
|
|
475
|
+
{ date: '2025-05-13', totalOrders: 1, totalItems: 0 },
|
|
476
|
+
{ date: '2025-05-14', totalOrders: 1, totalItems: 0 },
|
|
477
|
+
]
|
|
478
|
+
);
|
|
479
|
+
|
|
480
|
+
expect(result).toBe('2025-05-15T21:00:00Z');
|
|
481
|
+
});
|
|
482
|
+
|
|
483
|
+
test('pickEndDate - returns base date when no capacity counts are provided', () => {
|
|
484
|
+
const schedule = {
|
|
485
|
+
addDays: 0,
|
|
486
|
+
readyHour: { hour: 17, minute: 0 },
|
|
487
|
+
skipDays: [],
|
|
488
|
+
cutHour: { hour: 16, minute: 0 },
|
|
489
|
+
cutDay: 1,
|
|
490
|
+
};
|
|
491
|
+
|
|
492
|
+
const pricingService = usePricing(
|
|
493
|
+
getDefaultSettings([schedule], [{ day: 2, maxOrders: 1 }])
|
|
494
|
+
);
|
|
495
|
+
|
|
496
|
+
const now = moment('2025-05-13T15:00:00Z').tz('America/New_York');
|
|
497
|
+
jest.spyOn(moment, 'now').mockImplementation(() => now.valueOf());
|
|
498
|
+
|
|
499
|
+
const result = pricingService.store.pickEndDate(
|
|
500
|
+
undefined,
|
|
501
|
+
undefined,
|
|
502
|
+
undefined,
|
|
503
|
+
[]
|
|
504
|
+
);
|
|
505
|
+
|
|
506
|
+
expect(result).toBe('2025-05-13T21:00:00Z');
|
|
507
|
+
});
|
|
302
508
|
});
|
package/lib/constants/index.js
CHANGED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
module.exports = ({ utils, actions }) => {
|
|
2
|
+
const { math } = utils;
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Distributes a refund amount across a set of payment invoices.
|
|
6
|
+
*
|
|
7
|
+
* For each invoice, it resolves the live state of each item, computes
|
|
8
|
+
* the refundable amounts (order-independent), and builds the refund entries.
|
|
9
|
+
*
|
|
10
|
+
* Returns an array of refund invoices ready to be persisted — each with a
|
|
11
|
+
* negative amount (the refund) and updated paymentOrderItems.
|
|
12
|
+
*/
|
|
13
|
+
return function applyRefundToInvoices(
|
|
14
|
+
paymentInvoices,
|
|
15
|
+
ordersById,
|
|
16
|
+
totalToRefund
|
|
17
|
+
) {
|
|
18
|
+
let remaining = totalToRefund;
|
|
19
|
+
|
|
20
|
+
return paymentInvoices
|
|
21
|
+
.filter(({ amount }) => amount > 0)
|
|
22
|
+
.map(({ amount, paymentOrderItems, ...rest }) => {
|
|
23
|
+
if (remaining <= 0) return null;
|
|
24
|
+
|
|
25
|
+
const itemStates = (paymentOrderItems || []).map(item =>
|
|
26
|
+
actions.resolveItemState(item, ordersById)
|
|
27
|
+
);
|
|
28
|
+
const refundables = actions.computeRefundables(itemStates, remaining);
|
|
29
|
+
|
|
30
|
+
let invoiceRefundAmount = 0;
|
|
31
|
+
const itemRefunds = itemStates
|
|
32
|
+
.map((state, i) => {
|
|
33
|
+
const refundable = refundables[i];
|
|
34
|
+
if (refundable === 0) return null;
|
|
35
|
+
invoiceRefundAmount = math.add(invoiceRefundAmount, refundable);
|
|
36
|
+
return actions.buildItemRefund(state, refundable);
|
|
37
|
+
})
|
|
38
|
+
.filter(Boolean);
|
|
39
|
+
|
|
40
|
+
remaining = math.sub(remaining, invoiceRefundAmount);
|
|
41
|
+
|
|
42
|
+
if (invoiceRefundAmount <= 0) return null;
|
|
43
|
+
|
|
44
|
+
return {
|
|
45
|
+
...rest,
|
|
46
|
+
amount: -invoiceRefundAmount,
|
|
47
|
+
paymentOrderItems: itemRefunds,
|
|
48
|
+
};
|
|
49
|
+
})
|
|
50
|
+
.filter(Boolean);
|
|
51
|
+
};
|
|
52
|
+
};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
module.exports = ({ utils }) => {
|
|
2
|
+
const { math } = utils;
|
|
3
|
+
|
|
4
|
+
return function buildItemRefund(
|
|
5
|
+
{ item, itemTotal, itemTotalPaid, currentStatus },
|
|
6
|
+
refundable
|
|
7
|
+
) {
|
|
8
|
+
const nextTotalPaid = math.sub(itemTotalPaid, refundable);
|
|
9
|
+
return {
|
|
10
|
+
...item,
|
|
11
|
+
total: itemTotal,
|
|
12
|
+
totalPaid: itemTotalPaid,
|
|
13
|
+
amount: -refundable,
|
|
14
|
+
status: {
|
|
15
|
+
...currentStatus,
|
|
16
|
+
paid: {
|
|
17
|
+
...((currentStatus && currentStatus.paid) || {}),
|
|
18
|
+
value: nextTotalPaid >= itemTotal,
|
|
19
|
+
},
|
|
20
|
+
},
|
|
21
|
+
};
|
|
22
|
+
};
|
|
23
|
+
};
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
module.exports = ({ utils }) => {
|
|
2
|
+
const { math } = utils;
|
|
3
|
+
|
|
4
|
+
return function computeRefundables(itemStates, totalToRefund) {
|
|
5
|
+
const refundables = new Array(itemStates.length).fill(0);
|
|
6
|
+
|
|
7
|
+
if (totalToRefund <= 0) return refundables;
|
|
8
|
+
|
|
9
|
+
// Only partially-paid underpaid items are eligible to absorb redistribution credits.
|
|
10
|
+
const eligibleDeficit = itemStates.reduce(
|
|
11
|
+
(sum, { excess, itemTotalPaid }) => {
|
|
12
|
+
const isPartiallyPaidAndUnderpaid = excess < 0 && itemTotalPaid > 0;
|
|
13
|
+
return isPartiallyPaidAndUnderpaid
|
|
14
|
+
? math.add(sum, math.sub(0, excess))
|
|
15
|
+
: sum;
|
|
16
|
+
},
|
|
17
|
+
0
|
|
18
|
+
);
|
|
19
|
+
|
|
20
|
+
// Cap total drawn from overpaid items so the net refund never exceeds totalToRefund.
|
|
21
|
+
let maxFromOverpaid = math.add(totalToRefund, eligibleDeficit);
|
|
22
|
+
let tempRemaining = totalToRefund;
|
|
23
|
+
|
|
24
|
+
// Pass 1: refund each overpaid item's excess, capped by maxFromOverpaid
|
|
25
|
+
itemStates.forEach(({ excess }, i) => {
|
|
26
|
+
if (excess > 0 && maxFromOverpaid > 0) {
|
|
27
|
+
const refund = excess > maxFromOverpaid ? maxFromOverpaid : excess;
|
|
28
|
+
refundables[i] = refund;
|
|
29
|
+
tempRemaining = math.sub(tempRemaining, refund);
|
|
30
|
+
maxFromOverpaid = math.sub(maxFromOverpaid, refund);
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
if (tempRemaining < 0) {
|
|
35
|
+
// Pass 2a: over-refunded — spread surplus as credits to partially-paid underpaid items
|
|
36
|
+
let surplus = math.sub(0, tempRemaining);
|
|
37
|
+
itemStates.forEach(({ excess, itemTotalPaid }, i) => {
|
|
38
|
+
if (excess < 0 && itemTotalPaid > 0 && surplus > 0) {
|
|
39
|
+
const deficit = math.sub(0, excess);
|
|
40
|
+
const credit = surplus > deficit ? deficit : surplus;
|
|
41
|
+
refundables[i] = -credit;
|
|
42
|
+
surplus = math.sub(surplus, credit);
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
} else if (tempRemaining > 0) {
|
|
46
|
+
// Pass 2b: under-refunded — draw remainder from non-overpaid items (totalPaid > 0)
|
|
47
|
+
itemStates.forEach(({ itemTotalPaid, excess }, i) => {
|
|
48
|
+
if (excess <= 0 && itemTotalPaid > 0 && tempRemaining > 0) {
|
|
49
|
+
const refund =
|
|
50
|
+
tempRemaining > itemTotalPaid ? itemTotalPaid : tempRemaining;
|
|
51
|
+
refundables[i] = refund;
|
|
52
|
+
tempRemaining = math.sub(tempRemaining, refund);
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return refundables;
|
|
58
|
+
};
|
|
59
|
+
};
|
package/lib/invoice/index.js
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
const getStatusByItems = require('./getStatusByItems');
|
|
2
2
|
const getTotalByItems = require('./getTotalByItems');
|
|
3
|
+
const resolveItemState = require('./resolveItemState');
|
|
4
|
+
const computeRefundables = require('./computeRefundables');
|
|
5
|
+
const buildItemRefund = require('./buildItemRefund');
|
|
6
|
+
const applyRefundToInvoices = require('./applyRefundToInvoices');
|
|
3
7
|
|
|
4
8
|
const invoiceActions = (deps = {}) => {
|
|
5
9
|
const actions = {};
|
|
@@ -12,6 +16,10 @@ const invoiceActions = (deps = {}) => {
|
|
|
12
16
|
const freezedActions = Object.freeze({
|
|
13
17
|
getStatusByItems: getStatusByItems(innerDeps),
|
|
14
18
|
getTotalByItems: getTotalByItems(innerDeps),
|
|
19
|
+
resolveItemState: resolveItemState(innerDeps),
|
|
20
|
+
computeRefundables: computeRefundables(innerDeps),
|
|
21
|
+
buildItemRefund: buildItemRefund(innerDeps),
|
|
22
|
+
applyRefundToInvoices: applyRefundToInvoices(innerDeps),
|
|
15
23
|
});
|
|
16
24
|
|
|
17
25
|
Object.keys(freezedActions).forEach(actionName => {
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
module.exports = ({ utils }) => {
|
|
2
|
+
const { math } = utils;
|
|
3
|
+
|
|
4
|
+
return function resolveItemState(paymentOrderItem, ordersById) {
|
|
5
|
+
const order = (ordersById && ordersById[paymentOrderItem.orderId]) || {};
|
|
6
|
+
const orderItem = (order.items || []).find(
|
|
7
|
+
each => each._id === paymentOrderItem.orderItemId
|
|
8
|
+
);
|
|
9
|
+
|
|
10
|
+
const itemTotal =
|
|
11
|
+
(orderItem && orderItem.total) || paymentOrderItem.total || 0;
|
|
12
|
+
const itemTotalPaid =
|
|
13
|
+
(orderItem && orderItem.totalPaid) || paymentOrderItem.totalPaid || 0;
|
|
14
|
+
const currentStatus =
|
|
15
|
+
(orderItem && orderItem.status) || paymentOrderItem.status || {};
|
|
16
|
+
|
|
17
|
+
return {
|
|
18
|
+
item: paymentOrderItem,
|
|
19
|
+
itemTotal,
|
|
20
|
+
itemTotalPaid,
|
|
21
|
+
currentStatus,
|
|
22
|
+
excess: math.sub(itemTotalPaid, itemTotal),
|
|
23
|
+
};
|
|
24
|
+
};
|
|
25
|
+
};
|
package/lib/order/addItem.js
CHANGED
|
@@ -227,6 +227,8 @@ module.exports = ({ actions, itemActions, modifierActions, settings, _ }) => {
|
|
|
227
227
|
originalItem: item,
|
|
228
228
|
});
|
|
229
229
|
|
|
230
|
+
if (actions.hasSerial({ order, item: orderItem })) return orderProp;
|
|
231
|
+
|
|
230
232
|
const params = addOrderItem({
|
|
231
233
|
order,
|
|
232
234
|
item: orderItem,
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
module.exports = ({ _, moment }) =>
|
|
2
|
+
function getReadyDateCountsByDate(counts = []) {
|
|
3
|
+
const countsByDate = new Map();
|
|
4
|
+
|
|
5
|
+
if (!Array.isArray(counts) || !counts.length) return countsByDate;
|
|
6
|
+
|
|
7
|
+
counts.forEach(dayCount => {
|
|
8
|
+
const rawDate = _.get(dayCount, 'date');
|
|
9
|
+
if (!rawDate) return;
|
|
10
|
+
|
|
11
|
+
const utcDateKey = moment.utc(rawDate).format('YYYY-MM-DD');
|
|
12
|
+
if (!utcDateKey || utcDateKey === 'Invalid date') return;
|
|
13
|
+
|
|
14
|
+
const currentValue = countsByDate.get(utcDateKey) || {
|
|
15
|
+
totalOrders: 0,
|
|
16
|
+
totalItems: 0,
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const nextOrders = Number(_.get(dayCount, 'totalOrders'));
|
|
20
|
+
const nextItems = Number(_.get(dayCount, 'totalItems'));
|
|
21
|
+
|
|
22
|
+
countsByDate.set(utcDateKey, {
|
|
23
|
+
totalOrders:
|
|
24
|
+
currentValue.totalOrders +
|
|
25
|
+
(Number.isFinite(nextOrders) && nextOrders > 0 ? nextOrders : 0),
|
|
26
|
+
totalItems:
|
|
27
|
+
currentValue.totalItems +
|
|
28
|
+
(Number.isFinite(nextItems) && nextItems > 0 ? nextItems : 0),
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
return countsByDate;
|
|
33
|
+
};
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
module.exports = ({ _, moment, settings, actions }) =>
|
|
2
|
+
function getSchedule(schedules, isoWeekday, fromDate) {
|
|
3
|
+
const timezone = actions.getStoreTimezone();
|
|
4
|
+
let todayTZ = moment().tz(timezone);
|
|
5
|
+
|
|
6
|
+
if (fromDate) {
|
|
7
|
+
todayTZ = moment(fromDate).tz(timezone);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const day = isoWeekday || todayTZ.isoWeekday(); // monday 1 - sunday 7
|
|
11
|
+
|
|
12
|
+
let activeSchedules = _.get(settings, 'order.schedules', []).filter(
|
|
13
|
+
item => !item.departments || !item.departments.length
|
|
14
|
+
);
|
|
15
|
+
|
|
16
|
+
if (schedules && schedules.length) {
|
|
17
|
+
activeSchedules = schedules;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
if (!activeSchedules.length) {
|
|
21
|
+
return {
|
|
22
|
+
todayTZ,
|
|
23
|
+
skipDays: [],
|
|
24
|
+
cutDay: 0,
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const schedule = activeSchedules.find(item => {
|
|
29
|
+
const { dayOfWeek } = item || {};
|
|
30
|
+
if (!dayOfWeek || !dayOfWeek.length) return true;
|
|
31
|
+
return dayOfWeek.includes(day);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
if (schedule) {
|
|
35
|
+
return {
|
|
36
|
+
...schedule,
|
|
37
|
+
todayTZ,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
for (let offset = 1; offset <= 7; offset += 1) {
|
|
42
|
+
const nextIsoWeekday = ((day - 1 + offset) % 7) + 1;
|
|
43
|
+
const nextSchedule = activeSchedules.find(item => {
|
|
44
|
+
const { dayOfWeek } = item || {};
|
|
45
|
+
return Array.isArray(dayOfWeek) && dayOfWeek.includes(nextIsoWeekday);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
if (nextSchedule) {
|
|
49
|
+
return {
|
|
50
|
+
...nextSchedule,
|
|
51
|
+
todayTZ: todayTZ.clone().add(offset, 'days'),
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return {
|
|
57
|
+
todayTZ,
|
|
58
|
+
skipDays: [],
|
|
59
|
+
cutDay: 0,
|
|
60
|
+
};
|
|
61
|
+
};
|
package/lib/store/index.js
CHANGED
|
@@ -1,6 +1,14 @@
|
|
|
1
1
|
//
|
|
2
2
|
const getScheduleByCustomer = require('./getScheduleByCustomer');
|
|
3
3
|
const isNeareastMultiple = require('./isNeareastMultiple');
|
|
4
|
+
const getClosedDays = require('./getClosedDays');
|
|
5
|
+
const getReadyDateCapacityDays = require('./getReadyDateCapacityDays');
|
|
6
|
+
const getReadyDateCountsByDate = require('./getReadyDateCountsByDate');
|
|
7
|
+
const getSchedule = require('./getSchedule');
|
|
8
|
+
const getStoreTimezone = require('./getStoreTimezone');
|
|
9
|
+
const hasReadyDateCapacitySettings = require('./hasReadyDateCapacitySettings');
|
|
10
|
+
const normalizeLimit = require('./normalizeLimit');
|
|
11
|
+
const pickBaseEndDate = require('./pickBaseEndDate');
|
|
4
12
|
const pickEndDate = require('./pickEndDate');
|
|
5
13
|
const pickEndDateByCustomer = require('./pickEndDateByCustomer');
|
|
6
14
|
|
|
@@ -13,8 +21,16 @@ const storeActions = (deps = {}) => {
|
|
|
13
21
|
};
|
|
14
22
|
|
|
15
23
|
const freezedActions = Object.freeze({
|
|
24
|
+
getClosedDays: getClosedDays(innerDeps),
|
|
25
|
+
getReadyDateCapacityDays: getReadyDateCapacityDays(innerDeps),
|
|
26
|
+
getReadyDateCountsByDate: getReadyDateCountsByDate(innerDeps),
|
|
16
27
|
getScheduleByCustomer: getScheduleByCustomer(innerDeps),
|
|
28
|
+
getSchedule: getSchedule(innerDeps),
|
|
29
|
+
getStoreTimezone: getStoreTimezone(innerDeps),
|
|
30
|
+
hasReadyDateCapacitySettings: hasReadyDateCapacitySettings(innerDeps),
|
|
17
31
|
isNeareastMultiple: isNeareastMultiple(innerDeps),
|
|
32
|
+
normalizeLimit: normalizeLimit(innerDeps),
|
|
33
|
+
pickBaseEndDate: pickBaseEndDate(innerDeps),
|
|
18
34
|
pickEndDate: pickEndDate(innerDeps),
|
|
19
35
|
pickEndDateByCustomer: pickEndDateByCustomer(innerDeps),
|
|
20
36
|
});
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
const { MAX_ADD_DAYS } = require('../constants/Store');
|
|
2
|
+
|
|
3
|
+
module.exports = ({ moment, actions }) =>
|
|
4
|
+
function pickBaseEndDate(schedules, isoWeekday, fromDate) {
|
|
5
|
+
const {
|
|
6
|
+
addDays: addDaysParam,
|
|
7
|
+
readyHour,
|
|
8
|
+
cutHour,
|
|
9
|
+
cutDay,
|
|
10
|
+
skipDays,
|
|
11
|
+
todayTZ,
|
|
12
|
+
} = actions.getSchedule(schedules, isoWeekday, fromDate);
|
|
13
|
+
|
|
14
|
+
let endDateTZ = todayTZ.clone();
|
|
15
|
+
const closedDays = actions.getClosedDays();
|
|
16
|
+
const todayHours = todayTZ.get('hours');
|
|
17
|
+
const todayMinutes = todayTZ.get('minutes');
|
|
18
|
+
|
|
19
|
+
let addDays = 0;
|
|
20
|
+
|
|
21
|
+
if (
|
|
22
|
+
!fromDate &&
|
|
23
|
+
typeof addDaysParam === 'number' &&
|
|
24
|
+
addDaysParam <= MAX_ADD_DAYS
|
|
25
|
+
) {
|
|
26
|
+
addDays = addDaysParam;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (
|
|
30
|
+
(cutHour && todayHours > cutHour.hour) ||
|
|
31
|
+
(cutHour && todayHours >= cutHour.hour && todayMinutes > cutHour.minute)
|
|
32
|
+
) {
|
|
33
|
+
endDateTZ = endDateTZ.add(cutDay, 'days');
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (readyHour && readyHour.hour !== undefined)
|
|
37
|
+
endDateTZ.set('hour', readyHour.hour);
|
|
38
|
+
if (readyHour && readyHour.minute !== undefined)
|
|
39
|
+
endDateTZ.set('minute', readyHour.minute);
|
|
40
|
+
|
|
41
|
+
endDateTZ.set('second', 0);
|
|
42
|
+
|
|
43
|
+
const isSkipDay = endDateIsoWeekDay => {
|
|
44
|
+
if (skipDays && skipDays.length >= 7) return false;
|
|
45
|
+
|
|
46
|
+
return skipDays && skipDays.includes(endDateIsoWeekDay);
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const isClosedDay = endDate =>
|
|
50
|
+
closedDays.some(
|
|
51
|
+
closedDate => endDate.format('YYYY-MM-DD') === closedDate.date
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
let addedDays = 0;
|
|
55
|
+
|
|
56
|
+
while (
|
|
57
|
+
addedDays < addDays ||
|
|
58
|
+
isSkipDay(endDateTZ.isoWeekday()) ||
|
|
59
|
+
isClosedDay(endDateTZ)
|
|
60
|
+
) {
|
|
61
|
+
if (
|
|
62
|
+
endDateTZ.isSame(todayTZ, 'day') ||
|
|
63
|
+
(!isSkipDay(endDateTZ.isoWeekday()) && !isClosedDay(endDateTZ))
|
|
64
|
+
) {
|
|
65
|
+
addedDays += 1;
|
|
66
|
+
}
|
|
67
|
+
endDateTZ.add(1, 'days');
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return moment.utc(endDateTZ).format();
|
|
71
|
+
};
|
package/lib/store/pickEndDate.js
CHANGED
|
@@ -1,108 +1,82 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
1
|
+
const { MAX_ADD_DAYS } = require('../constants/Store');
|
|
2
|
+
|
|
3
|
+
module.exports = ({ _, moment, actions }) =>
|
|
4
|
+
function pickEndDate(
|
|
5
|
+
schedules,
|
|
6
|
+
isoWeekday,
|
|
7
|
+
fromDate,
|
|
8
|
+
readyDateCapacityCounts = []
|
|
9
|
+
) {
|
|
10
|
+
const timezone = actions.getStoreTimezone();
|
|
11
|
+
|
|
12
|
+
let candidateDate = actions.pickBaseEndDate(
|
|
13
|
+
schedules,
|
|
14
|
+
isoWeekday,
|
|
15
|
+
fromDate
|
|
16
|
+
);
|
|
17
|
+
|
|
18
|
+
const readyDateCapacityDays = actions.getReadyDateCapacityDays();
|
|
19
|
+
if (!actions.hasReadyDateCapacitySettings(readyDateCapacityDays))
|
|
20
|
+
return candidateDate;
|
|
21
|
+
|
|
22
|
+
const countsByDate = actions.getReadyDateCountsByDate(
|
|
23
|
+
readyDateCapacityCounts
|
|
24
|
+
);
|
|
25
|
+
if (!countsByDate.size) return candidateDate;
|
|
26
|
+
|
|
27
|
+
const inspectedDays = new Set();
|
|
28
|
+
|
|
29
|
+
for (let index = 0; index < MAX_ADD_DAYS; index += 1) {
|
|
30
|
+
const candidateMomentTZ = moment(candidateDate).tz(timezone);
|
|
31
|
+
const candidateLocalDateKey = candidateMomentTZ.format('YYYY-MM-DD');
|
|
32
|
+
|
|
33
|
+
if (inspectedDays.has(candidateLocalDateKey)) break;
|
|
34
|
+
inspectedDays.add(candidateLocalDateKey);
|
|
35
|
+
|
|
36
|
+
const capacitySettingsForDay = readyDateCapacityDays.find(
|
|
37
|
+
day => Number(_.get(day, 'day')) === candidateMomentTZ.isoWeekday()
|
|
38
|
+
);
|
|
23
39
|
|
|
24
|
-
|
|
25
|
-
const { dayOfWeek } = item || {};
|
|
26
|
-
if (!dayOfWeek || !dayOfWeek.length) return true;
|
|
27
|
-
return dayOfWeek.includes(day);
|
|
28
|
-
});
|
|
40
|
+
if (!capacitySettingsForDay) return candidateDate;
|
|
29
41
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
42
|
+
const maxOrders = actions.normalizeLimit(
|
|
43
|
+
_.get(capacitySettingsForDay, 'maxOrders')
|
|
44
|
+
);
|
|
45
|
+
const maxItems = actions.normalizeLimit(
|
|
46
|
+
_.get(capacitySettingsForDay, 'maxItems')
|
|
47
|
+
);
|
|
35
48
|
|
|
36
|
-
|
|
37
|
-
const schedule = _.get(settings, 'schedule', {});
|
|
38
|
-
const { closed = [] } = schedule;
|
|
39
|
-
if (!Array.isArray(closed)) return [];
|
|
40
|
-
return closed;
|
|
41
|
-
};
|
|
49
|
+
if (maxOrders <= 0 && maxItems <= 0) return candidateDate;
|
|
42
50
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
cutDay,
|
|
51
|
-
skipDays,
|
|
52
|
-
todayTZ,
|
|
53
|
-
} = getSchedule(schedules, isoWeekday);
|
|
54
|
-
|
|
55
|
-
let endDateTZ = todayTZ.clone();
|
|
56
|
-
const closedDays = getClosedDays();
|
|
57
|
-
const todayHours = todayTZ.get('hours');
|
|
58
|
-
const todayMinutes = todayTZ.get('minutes');
|
|
59
|
-
|
|
60
|
-
const addDays =
|
|
61
|
-
typeof addDaysParam !== 'number' || addDaysParam > MAX_ADD_DAYS
|
|
62
|
-
? 0
|
|
63
|
-
: addDaysParam;
|
|
64
|
-
|
|
65
|
-
if (
|
|
66
|
-
(cutHour && todayHours > cutHour.hour) ||
|
|
67
|
-
(cutHour && todayHours >= cutHour.hour && todayMinutes > cutHour.minute)
|
|
68
|
-
) {
|
|
69
|
-
endDateTZ = endDateTZ.add(cutDay, 'days');
|
|
70
|
-
}
|
|
51
|
+
const candidateUTCDateKey = moment
|
|
52
|
+
.utc(candidateDate)
|
|
53
|
+
.format('YYYY-MM-DD');
|
|
54
|
+
const dayCount = countsByDate.get(candidateUTCDateKey) || {
|
|
55
|
+
totalOrders: 0,
|
|
56
|
+
totalItems: 0,
|
|
57
|
+
};
|
|
71
58
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
endDateTZ.set('minute', readyHour.minute);
|
|
59
|
+
const isUnavailable =
|
|
60
|
+
(maxOrders > 0 && dayCount.totalOrders >= maxOrders) ||
|
|
61
|
+
(maxItems > 0 && dayCount.totalItems >= maxItems);
|
|
76
62
|
|
|
77
|
-
|
|
63
|
+
if (!isUnavailable) return candidateDate;
|
|
78
64
|
|
|
79
|
-
|
|
80
|
-
|
|
65
|
+
const nextDateSeed = candidateMomentTZ
|
|
66
|
+
.clone()
|
|
67
|
+
.add(1, 'day')
|
|
68
|
+
.startOf('day')
|
|
69
|
+
.toISOString();
|
|
81
70
|
|
|
82
|
-
|
|
83
|
-
|
|
71
|
+
const { readyHour, cutHour, cutDay, skipDays } =
|
|
72
|
+
actions.getSchedule(schedules, undefined, candidateDate) || {};
|
|
84
73
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
74
|
+
candidateDate = actions.pickBaseEndDate(
|
|
75
|
+
[{ readyHour, cutHour, cutDay, skipDays }],
|
|
76
|
+
undefined,
|
|
77
|
+
nextDateSeed
|
|
88
78
|
);
|
|
89
|
-
|
|
90
|
-
let addedDays = 0;
|
|
91
|
-
|
|
92
|
-
while (
|
|
93
|
-
addedDays < addDays ||
|
|
94
|
-
isSkipDay(endDateTZ.isoWeekday()) ||
|
|
95
|
-
isClosedDay(endDateTZ)
|
|
96
|
-
) {
|
|
97
|
-
if (
|
|
98
|
-
endDateTZ.isSame(todayTZ, 'day') ||
|
|
99
|
-
(!isSkipDay(endDateTZ.isoWeekday()) && !isClosedDay(endDateTZ))
|
|
100
|
-
) {
|
|
101
|
-
addedDays += 1;
|
|
102
|
-
}
|
|
103
|
-
endDateTZ.add(1, 'days');
|
|
104
79
|
}
|
|
105
80
|
|
|
106
|
-
return
|
|
81
|
+
return candidateDate;
|
|
107
82
|
};
|
|
108
|
-
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@darkpos/pricing",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.147",
|
|
4
4
|
"description": "Pricing calculator",
|
|
5
5
|
"author": "Dark POS",
|
|
6
6
|
"license": "ISC",
|
|
@@ -54,5 +54,5 @@
|
|
|
54
54
|
"supertest": "^6.2.3",
|
|
55
55
|
"supervisor": "^0.12.0"
|
|
56
56
|
},
|
|
57
|
-
"gitHead": "
|
|
57
|
+
"gitHead": "c983de0f3b8d4978e2fb0b68551ba347dbdc4d55"
|
|
58
58
|
}
|