@darkpos/pricing 1.0.152 → 1.0.153

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.
@@ -0,0 +1,387 @@
1
+ const usePricing = require('../../index');
2
+ const mockStores = require('../mocks/stores');
3
+
4
+ const session = {
5
+ store: mockStores[0],
6
+ };
7
+
8
+ const pricingService = usePricing(session);
9
+
10
+ const createOptions = items => ({
11
+ 'order-1': {
12
+ items,
13
+ },
14
+ });
15
+
16
+ describe('Apply refund', () => {
17
+ test('keeps applyPayment working for positive amounts', () => {
18
+ const options = createOptions({
19
+ 'item-1': {
20
+ total: 8,
21
+ totalPaid: 0,
22
+ status: {},
23
+ },
24
+ 'item-2': {
25
+ total: 7,
26
+ totalPaid: 0,
27
+ status: {},
28
+ },
29
+ });
30
+
31
+ const result = pricingService.order.applyPayment(options, 10);
32
+
33
+ expect(result['order-1'].items['item-1']).toMatchObject({
34
+ orderId: 'order-1',
35
+ amount: 8,
36
+ totalPaid: 8,
37
+ status: {
38
+ paid: {
39
+ value: true,
40
+ },
41
+ },
42
+ });
43
+
44
+ expect(result['order-1'].items['item-2']).toMatchObject({
45
+ orderId: 'order-1',
46
+ amount: 2,
47
+ totalPaid: 2,
48
+ status: {
49
+ paid: {
50
+ value: false,
51
+ },
52
+ },
53
+ });
54
+ });
55
+
56
+ test('applies refunds when amount to process is negative', () => {
57
+ const options = createOptions({
58
+ 'item-1': {
59
+ total: 8,
60
+ totalPaid: 8,
61
+ status: {
62
+ paid: {
63
+ value: true,
64
+ date: new Date('2024-01-01T00:00:00.000Z'),
65
+ },
66
+ },
67
+ },
68
+ 'item-2': {
69
+ total: 7,
70
+ totalPaid: 7,
71
+ status: {
72
+ paid: {
73
+ value: true,
74
+ date: new Date('2024-01-01T00:00:00.000Z'),
75
+ },
76
+ },
77
+ },
78
+ });
79
+
80
+ const result = pricingService.order.applyRefund(options, -10);
81
+
82
+ expect(result.refundedAmount).toBe(10);
83
+
84
+ expect(result.options['order-1'].items['item-1']).toMatchObject({
85
+ orderId: 'order-1',
86
+ amount: -8,
87
+ totalPaid: 0,
88
+ status: {
89
+ paid: {
90
+ value: false,
91
+ },
92
+ },
93
+ });
94
+
95
+ expect(result.options['order-1'].items['item-2']).toMatchObject({
96
+ orderId: 'order-1',
97
+ amount: -2,
98
+ totalPaid: 5,
99
+ status: {
100
+ paid: {
101
+ value: false,
102
+ },
103
+ },
104
+ });
105
+ });
106
+
107
+ test('accepts positive refund amounts and only applies the visible overpayment on current items', () => {
108
+ const options = createOptions({
109
+ 'item-1': {
110
+ total: 1,
111
+ totalPaid: 5,
112
+ status: {
113
+ paid: {
114
+ value: true,
115
+ date: new Date('2024-01-01T00:00:00.000Z'),
116
+ },
117
+ },
118
+ },
119
+ 'item-2': {
120
+ total: 1,
121
+ totalPaid: 5,
122
+ status: {
123
+ paid: {
124
+ value: true,
125
+ date: new Date('2024-01-01T00:00:00.000Z'),
126
+ },
127
+ },
128
+ },
129
+ });
130
+
131
+ const result = pricingService.order.applyRefund(options, 3);
132
+
133
+ expect(result.refundedAmount).toBe(3);
134
+
135
+ expect(result.options['order-1'].items['item-1']).toMatchObject({
136
+ orderId: 'order-1',
137
+ amount: -3,
138
+ totalPaid: 2,
139
+ status: {
140
+ paid: {
141
+ value: true,
142
+ },
143
+ },
144
+ });
145
+
146
+ expect(result.options['order-1'].items['item-2']).toMatchObject({
147
+ orderId: 'order-1',
148
+ amount: 0,
149
+ totalPaid: 5,
150
+ status: {
151
+ paid: {
152
+ value: true,
153
+ },
154
+ },
155
+ });
156
+ });
157
+
158
+ test('normalizes overpaid items after refund so no item keeps totalPaid above total', () => {
159
+ const options = createOptions({
160
+ '698a364799a15511959c9135': {
161
+ status: {
162
+ picked: {
163
+ value: false,
164
+ date: '',
165
+ },
166
+ paid: {
167
+ value: true,
168
+ date: new Date('2026-04-01T16:32:04.514Z'),
169
+ },
170
+ tracker: [],
171
+ },
172
+ total: 1.05,
173
+ totalPaid: 14.7,
174
+ },
175
+ '698a364799a15511959c9130': {
176
+ status: {
177
+ picked: {
178
+ value: false,
179
+ date: '',
180
+ },
181
+ paid: {
182
+ value: false,
183
+ date: new Date('2026-04-01T16:32:04.514Z'),
184
+ },
185
+ tracker: [],
186
+ },
187
+ total: 1.05,
188
+ totalPaid: 0,
189
+ },
190
+ });
191
+
192
+ const result = pricingService.order.applyRefund(options, -12.6);
193
+
194
+ expect(result.refundedAmount).toBe(12.6);
195
+
196
+ expect(
197
+ result.options['order-1'].items['698a364799a15511959c9135']
198
+ ).toMatchObject({
199
+ orderId: 'order-1',
200
+ amount: -13.65,
201
+ totalPaid: 1.05,
202
+ status: {
203
+ paid: {
204
+ value: true,
205
+ },
206
+ },
207
+ });
208
+
209
+ expect(
210
+ result.options['order-1'].items['698a364799a15511959c9130']
211
+ ).toMatchObject({
212
+ orderId: 'order-1',
213
+ amount: 1.05,
214
+ totalPaid: 1.05,
215
+ status: {
216
+ paid: {
217
+ value: true,
218
+ },
219
+ },
220
+ });
221
+ });
222
+
223
+ test('ignores refund amounts larger than the refundable balance', () => {
224
+ const options = createOptions({
225
+ 'item-1': {
226
+ total: 8,
227
+ totalPaid: 8,
228
+ status: {
229
+ paid: {
230
+ value: true,
231
+ date: new Date('2024-01-01T00:00:00.000Z'),
232
+ },
233
+ },
234
+ },
235
+ 'item-2': {
236
+ total: 7,
237
+ totalPaid: 2,
238
+ status: {
239
+ paid: {
240
+ value: false,
241
+ date: new Date('2024-01-01T00:00:00.000Z'),
242
+ },
243
+ },
244
+ },
245
+ });
246
+
247
+ const result = pricingService.order.applyRefund(options, -100);
248
+
249
+ expect(result.refundedAmount).toBe(10);
250
+
251
+ expect(result.options['order-1'].items['item-1']).toMatchObject({
252
+ orderId: 'order-1',
253
+ amount: -8,
254
+ totalPaid: 0,
255
+ status: {
256
+ paid: {
257
+ value: false,
258
+ },
259
+ },
260
+ });
261
+
262
+ expect(result.options['order-1'].items['item-2']).toMatchObject({
263
+ orderId: 'order-1',
264
+ amount: -2,
265
+ totalPaid: 0,
266
+ status: {
267
+ paid: {
268
+ value: false,
269
+ },
270
+ },
271
+ });
272
+ });
273
+
274
+ test('refunds overpaid zero-total items before touching correctly paid items', () => {
275
+ const options = createOptions({
276
+ shorts: {
277
+ total: 6.3,
278
+ totalPaid: 6.3,
279
+ status: {
280
+ paid: {
281
+ value: true,
282
+ date: new Date('2024-01-01T00:00:00.000Z'),
283
+ },
284
+ },
285
+ },
286
+ jacket1: {
287
+ total: 0,
288
+ totalPaid: 8.93,
289
+ status: {
290
+ paid: {
291
+ value: true,
292
+ date: new Date('2024-01-01T00:00:00.000Z'),
293
+ },
294
+ },
295
+ },
296
+ longJacket: {
297
+ total: 0,
298
+ totalPaid: 12.6,
299
+ status: {
300
+ paid: {
301
+ value: true,
302
+ date: new Date('2024-01-01T00:00:00.000Z'),
303
+ },
304
+ },
305
+ },
306
+ jacket2: {
307
+ total: 0,
308
+ totalPaid: 8.93,
309
+ status: {
310
+ paid: {
311
+ value: true,
312
+ date: new Date('2024-01-01T00:00:00.000Z'),
313
+ },
314
+ },
315
+ },
316
+ coat: {
317
+ total: 13.65,
318
+ totalPaid: 3.24,
319
+ status: {
320
+ paid: {
321
+ value: false,
322
+ date: new Date('2024-01-01T00:00:00.000Z'),
323
+ },
324
+ },
325
+ },
326
+ });
327
+
328
+ const result = pricingService.order.applyRefund(options, 20.05);
329
+
330
+ expect(result.refundedAmount).toBe(20.05);
331
+
332
+ expect(result.options['order-1'].items.shorts).toMatchObject({
333
+ orderId: 'order-1',
334
+ amount: 0,
335
+ totalPaid: 6.3,
336
+ status: {
337
+ paid: {
338
+ value: true,
339
+ },
340
+ },
341
+ });
342
+
343
+ expect(result.options['order-1'].items.jacket1).toMatchObject({
344
+ orderId: 'order-1',
345
+ amount: -8.93,
346
+ totalPaid: 0,
347
+ status: {
348
+ paid: {
349
+ value: true,
350
+ },
351
+ },
352
+ });
353
+
354
+ expect(result.options['order-1'].items.longJacket).toMatchObject({
355
+ orderId: 'order-1',
356
+ amount: -12.6,
357
+ totalPaid: 0,
358
+ status: {
359
+ paid: {
360
+ value: true,
361
+ },
362
+ },
363
+ });
364
+
365
+ expect(result.options['order-1'].items.jacket2).toMatchObject({
366
+ orderId: 'order-1',
367
+ amount: -8.93,
368
+ totalPaid: 0,
369
+ status: {
370
+ paid: {
371
+ value: true,
372
+ },
373
+ },
374
+ });
375
+
376
+ expect(result.options['order-1'].items.coat).toMatchObject({
377
+ orderId: 'order-1',
378
+ amount: 10.41,
379
+ totalPaid: 13.65,
380
+ status: {
381
+ paid: {
382
+ value: true,
383
+ },
384
+ },
385
+ });
386
+ });
387
+ });
@@ -62,7 +62,7 @@ describe('getMaxAmountToRefund tests', () => {
62
62
  });
63
63
 
64
64
  describe('getRefundableOrderAmount tests', () => {
65
- test('Returns paid amount not attached to current items', () => {
65
+ test('Returns 0 when retained order payment still matches the remaining item total', () => {
66
66
  const order = {
67
67
  _id: 'order-1',
68
68
  totalPaid: 5,
@@ -77,10 +77,10 @@ describe('getRefundableOrderAmount tests', () => {
77
77
  ],
78
78
  };
79
79
 
80
- expect(pricingService.payment.getRefundableOrderAmount({ order })).toBe(5);
80
+ expect(pricingService.payment.getRefundableOrderAmount({ order })).toBe(0);
81
81
  });
82
82
 
83
- test('Returns only the excess from items that actually received payment', () => {
83
+ test('Returns the net order overpayment (totalPaid minus total), regardless of per-item breakdown', () => {
84
84
  const order = {
85
85
  _id: 'order-2',
86
86
  totalPaid: 14.7,
@@ -103,177 +103,107 @@ describe('getRefundableOrderAmount tests', () => {
103
103
  };
104
104
 
105
105
  expect(pricingService.payment.getRefundableOrderAmount({ order })).toBe(
106
- 13.65
106
+ 12.6
107
107
  );
108
108
  });
109
109
  });
110
110
 
111
- describe('getoverPaidOrders tests', () => {
112
- test('Collects refundable child orders from a mixed parent list', () => {
113
- const orders = [
114
- {
115
- _id: 'parent-1',
116
- isParent: true,
117
- orders: [
118
- {
119
- _id: 'child-1',
120
- totalPaid: 5,
121
- items: [
122
- {
123
- _id: 'item-1',
124
- total: 5,
125
- totalPaid: 0,
126
- status: { paid: { value: false } },
127
- },
128
- ],
129
- },
130
- {
131
- _id: 'child-2',
132
- totalPaid: 5,
133
- items: [
134
- {
135
- _id: 'item-2',
136
- total: 5,
137
- totalPaid: 5,
138
- status: { paid: { value: true } },
139
- },
140
- ],
141
- },
142
- ],
143
- },
144
- ];
111
+ describe('getOverPaidOrders tests', () => {
112
+ const baseOrder = pricingService.order.calculate({
113
+ _id: 'order-1',
114
+ items: [{ price: 30, quantity: 1, modifiers: [] }],
115
+ });
145
116
 
146
- expect(pricingService.payment.getOverPaidOrders({ orders })).toEqual({
147
- overPaidAmount: 5,
148
- overPaidOrders: [
149
- {
150
- _id: 'child-1',
151
- totalPaid: 5,
152
- items: [
153
- {
154
- _id: 'item-1',
155
- total: 5,
156
- totalPaid: 0,
157
- status: { paid: { value: false } },
158
- },
159
- ],
160
- refundableAmount: 5,
161
- },
162
- ],
117
+ test('includes order and accumulates overpaid amount when customer paid more than the order total', () => {
118
+ const order = { ...baseOrder, totalPaid: 50 };
119
+
120
+ const result = pricingService.payment.getOverPaidOrders({
121
+ orders: [order],
163
122
  });
123
+
124
+ expect(result.overPaidOrders).toHaveLength(1);
125
+ expect(result.overPaidAmount).toBe(20);
164
126
  });
165
127
 
166
- test('Ignores empty and non-refundable orders', () => {
167
- const orders = [
168
- null,
169
- {
170
- _id: 'order-1',
171
- totalPaid: 5,
172
- items: [
173
- {
174
- _id: 'item-1',
175
- total: 5,
176
- totalPaid: 5,
177
- status: { paid: { value: true } },
178
- },
179
- ],
180
- },
181
- {
182
- isParent: true,
183
- orders: [
184
- {
185
- _id: 'child-1',
186
- totalPaid: 1,
187
- items: [
188
- {
189
- _id: 'item-2',
190
- total: 1,
191
- totalPaid: 1,
192
- status: { paid: { value: true } },
193
- },
194
- ],
195
- },
196
- ],
197
- },
198
- ];
128
+ test('returns empty list and zero amount when customer paid exactly the order total', () => {
129
+ const order = { ...baseOrder, totalPaid: 30 };
199
130
 
200
- expect(pricingService.payment.getOverPaidOrders({ orders })).toEqual({
201
- overPaidAmount: 0,
202
- overPaidOrders: [],
131
+ const result = pricingService.payment.getOverPaidOrders({
132
+ orders: [order],
203
133
  });
134
+
135
+ expect(result.overPaidOrders).toHaveLength(0);
136
+ expect(result.overPaidAmount).toBe(0);
204
137
  });
205
138
  });
206
139
 
207
- describe('getMethodLabel tests', () => {
208
- test('Returns the configured label for the payment provider', () => {
209
- expect(
210
- pricingServiceMethodLabel.payment.getMethodLabel({
211
- paymentProvider: 'cash',
212
- })
213
- ).toBe('Cash');
140
+ describe('capOverPaidItem tests', () => {
141
+ test('caps totalPaid to item total and returns the excess as amountCapped', () => {
142
+ const item = { total: 10, totalPaid: 15 };
143
+
144
+ const result = pricingService.item.capOverPaidItem({ item });
145
+
146
+ expect(result.item.totalPaid).toBe(10);
147
+ expect(result.amountCapped).toBe(5);
214
148
  });
215
149
 
216
- test('Returns empty string when the provider is missing', () => {
217
- expect(
218
- pricingServiceMethodLabel.payment.getMethodLabel({
219
- paymentProvider: 'missing-provider',
220
- })
221
- ).toBe('');
150
+ test('returns item unchanged and amountCapped 0 when totalPaid does not exceed total', () => {
151
+ const item = { total: 10, totalPaid: 10 };
152
+
153
+ const result = pricingService.item.capOverPaidItem({ item });
154
+
155
+ expect(result.item.totalPaid).toBe(10);
156
+ expect(result.amountCapped).toBe(0);
222
157
  });
223
158
  });
224
159
 
225
- describe('appendIfOverPaid tests', () => {
226
- test('Appends the order with refundableAmount when the order is overpaid', () => {
227
- const currentOrder = {
160
+ describe('getTotalPaidToRedistribute tests', () => {
161
+ test('returns order with redistributableAmount when totalPaid exceeds item totalPaid sum', () => {
162
+ const order = {
228
163
  _id: 'order-1',
229
- displayId: '101',
230
164
  totalPaid: 5,
231
- items: [
232
- {
233
- _id: 'item-2',
234
- name: 'Simple Item 2',
235
- total: 5,
236
- totalPaid: 0,
237
- status: { paid: { value: false } },
238
- },
239
- ],
165
+ items: [{ totalPaid: 0 }, { totalPaid: 0 }],
240
166
  };
241
167
 
242
- expect(
243
- pricingService.payment.appendIfOverPaid({
244
- currentOrder,
245
- overPaidOrders: [{ _id: 'existing-order', refundableAmount: 1 }],
246
- })
247
- ).toEqual([
248
- { _id: 'existing-order', refundableAmount: 1 },
249
- {
250
- ...currentOrder,
251
- refundableAmount: 5,
252
- },
253
- ]);
168
+ const result = pricingService.order.getTotalPaidToRedistribute({
169
+ orders: [order],
170
+ });
171
+
172
+ expect(result.redistributableOrders).toHaveLength(1);
173
+ expect(result.redistributableOrders[0].redistributableAmount).toBe(5);
174
+ expect(result.redistributableAmount).toBe(5);
254
175
  });
255
176
 
256
- test('Leaves the collection unchanged when the order has no refundable amount', () => {
257
- const overPaidOrders = [{ _id: 'existing-order', refundableAmount: 1 }];
258
- const currentOrder = {
259
- _id: 'order-3',
260
- totalPaid: 5,
261
- items: [
262
- {
263
- _id: 'item-1',
264
- name: 'Simple Item 1',
265
- total: 5,
266
- totalPaid: 5,
267
- status: { paid: { value: true } },
268
- },
269
- ],
177
+ test('returns empty list and zero amount when all totalPaid is already distributed to items', () => {
178
+ const order = {
179
+ _id: 'order-1',
180
+ totalPaid: 10,
181
+ items: [{ totalPaid: 6 }, { totalPaid: 4 }],
270
182
  };
271
183
 
184
+ const result = pricingService.order.getTotalPaidToRedistribute({
185
+ orders: [order],
186
+ });
187
+
188
+ expect(result.redistributableOrders).toHaveLength(0);
189
+ expect(result.redistributableAmount).toBe(0);
190
+ });
191
+ });
192
+
193
+ describe('getMethodLabel tests', () => {
194
+ test('Returns the configured label for the payment provider', () => {
272
195
  expect(
273
- pricingService.payment.appendIfOverPaid({
274
- currentOrder,
275
- overPaidOrders,
196
+ pricingServiceMethodLabel.payment.getMethodLabel({
197
+ paymentProvider: 'cash',
276
198
  })
277
- ).toEqual(overPaidOrders);
199
+ ).toBe('Cash');
200
+ });
201
+
202
+ test('Returns empty string when the provider is missing', () => {
203
+ expect(
204
+ pricingServiceMethodLabel.payment.getMethodLabel({
205
+ paymentProvider: 'missing-provider',
206
+ })
207
+ ).toBe('');
278
208
  });
279
209
  });
@@ -1,7 +1,5 @@
1
1
  const getStatusByItems = require('./getStatusByItems');
2
2
  const getTotalByItems = require('./getTotalByItems');
3
- const resolveItemState = require('./resolveItemState');
4
- const computeRefundables = require('./computeRefundables');
5
3
 
6
4
  const invoiceActions = (deps = {}) => {
7
5
  const actions = {};
@@ -14,8 +12,6 @@ const invoiceActions = (deps = {}) => {
14
12
  const freezedActions = Object.freeze({
15
13
  getStatusByItems: getStatusByItems(innerDeps),
16
14
  getTotalByItems: getTotalByItems(innerDeps),
17
- resolveItemState: resolveItemState(innerDeps),
18
- computeRefundables: computeRefundables(innerDeps),
19
15
  });
20
16
 
21
17
  Object.keys(freezedActions).forEach(actionName => {
@@ -0,0 +1,14 @@
1
+ module.exports = ({ utils }) =>
2
+ function capOverPaidItem({ item }) {
3
+ const total = item.total || 0;
4
+
5
+ const totalPaid = item.totalPaid || 0;
6
+
7
+ if (totalPaid > total)
8
+ return {
9
+ item: { ...item, totalPaid: total },
10
+ amountCapped: utils.math.sub(totalPaid, total),
11
+ };
12
+
13
+ return { item, amountCapped: 0 };
14
+ };
package/lib/item/index.js CHANGED
@@ -84,6 +84,7 @@ const getAmountToPayById = require('./getAmountToPayById');
84
84
  const applyPayment = require('./applyPayment');
85
85
  const getBalanceForPaymentModifier = require('./getBalanceForPaymentModifier');
86
86
  const isOverpaid = require('./isOverpaid');
87
+ const capOverPaidItem = require('./capOverPaidItem');
87
88
 
88
89
  const itemActions = (deps = {}) => {
89
90
  const actions = {};
@@ -181,6 +182,7 @@ const itemActions = (deps = {}) => {
181
182
  applyPayment: applyPayment(innerDeps),
182
183
  getBalanceForPaymentModifier: getBalanceForPaymentModifier(innerDeps),
183
184
  isOverpaid: isOverpaid(innerDeps),
185
+ capOverPaidItem: capOverPaidItem(innerDeps),
184
186
  });
185
187
 
186
188
  Object.keys(freezedActions).forEach(actionName => {
@@ -0,0 +1,125 @@
1
+ module.exports = ({ utils, actions }) =>
2
+ function applyRefund(options, amountToProcess = 0) {
3
+ const optionsWithAmount = options;
4
+ let amount = Math.abs(amountToProcess || 0);
5
+ let refundedAmount = 0;
6
+
7
+ Object.keys(options).forEach(orderId => {
8
+ const order = options[orderId];
9
+ const { items } = order;
10
+ const itemIds = Object.keys(items);
11
+
12
+ itemIds.forEach(itemId => {
13
+ const item = items[itemId];
14
+
15
+ items[itemId] = {
16
+ ...item,
17
+ orderId,
18
+ amount: item.amount || 0,
19
+ };
20
+ });
21
+
22
+ itemIds.forEach(itemId => {
23
+ if (amount <= 0) return;
24
+
25
+ const item = items[itemId];
26
+ const overPaidAmount = utils.math.max(
27
+ utils.math.sub(item.totalPaid || 0, item.total || 0),
28
+ 0
29
+ );
30
+ const { refunded, item: updatedItem } = actions.refundOrderItem({
31
+ item,
32
+ orderId,
33
+ amountToRefund: utils.math.min(amount, overPaidAmount),
34
+ });
35
+
36
+ if (!updatedItem) return;
37
+
38
+ refundedAmount = utils.math.add(refundedAmount, refunded);
39
+ items[itemId] = updatedItem;
40
+ amount = utils.math.sub(amount, refunded);
41
+ });
42
+
43
+ itemIds.forEach(itemId => {
44
+ if (amount <= 0) return;
45
+
46
+ const item = items[itemId];
47
+ const { refunded, item: updatedItem } = actions.refundOrderItem({
48
+ item,
49
+ orderId,
50
+ amountToRefund: amount,
51
+ });
52
+
53
+ if (!updatedItem) return;
54
+
55
+ refundedAmount = utils.math.add(refundedAmount, refunded);
56
+ items[itemId] = updatedItem;
57
+ amount = utils.math.sub(amount, refunded);
58
+ });
59
+
60
+ itemIds.forEach((itemId, index) => {
61
+ const item = items[itemId];
62
+ let excess = utils.math.max(
63
+ utils.math.sub(item.totalPaid || 0, item.total || 0),
64
+ 0
65
+ );
66
+
67
+ if (excess <= 0) return;
68
+
69
+ for (
70
+ let nextIndex = index + 1;
71
+ nextIndex < itemIds.length;
72
+ nextIndex += 1
73
+ ) {
74
+ if (excess <= 0) break;
75
+
76
+ const nextItemId = itemIds[nextIndex];
77
+ const nextItem = items[nextItemId];
78
+ const capacity = utils.math.max(
79
+ utils.math.sub(nextItem.total || 0, nextItem.totalPaid || 0),
80
+ 0
81
+ );
82
+ const amountToMove = utils.math.min(excess, capacity);
83
+
84
+ if (amountToMove > 0) {
85
+ items[itemId] = {
86
+ ...items[itemId],
87
+ amount: utils.math.sub(items[itemId].amount || 0, amountToMove),
88
+ totalPaid: utils.math.sub(
89
+ items[itemId].totalPaid || 0,
90
+ amountToMove
91
+ ),
92
+ };
93
+
94
+ items[nextItemId] = {
95
+ ...nextItem,
96
+ amount: utils.math.add(nextItem.amount || 0, amountToMove),
97
+ totalPaid: utils.math.add(nextItem.totalPaid || 0, amountToMove),
98
+ };
99
+
100
+ excess = utils.math.sub(excess, amountToMove);
101
+ }
102
+ }
103
+ });
104
+
105
+ itemIds.forEach(itemId => {
106
+ const item = items[itemId];
107
+
108
+ items[itemId] = {
109
+ ...item,
110
+ status: {
111
+ ...item.status,
112
+ paid: {
113
+ value: (item.totalPaid || 0) >= (item.total || 0),
114
+ date: new Date(),
115
+ },
116
+ },
117
+ };
118
+ });
119
+ });
120
+
121
+ return {
122
+ options: optionsWithAmount,
123
+ refundedAmount,
124
+ };
125
+ };
@@ -0,0 +1,13 @@
1
+ module.exports = ({ itemActions, utils }) =>
2
+ function getOverPaidAmount({ order }) {
3
+ if (!order) return 0;
4
+
5
+ const { total: itemsTotal } = itemActions.getItemsTotals(order.items || []);
6
+
7
+ const orderTotalPaid = order.totalPaid || 0;
8
+
9
+ if (orderTotalPaid > itemsTotal)
10
+ return utils.math.sub(orderTotalPaid, itemsTotal);
11
+
12
+ return 0;
13
+ };
@@ -0,0 +1,41 @@
1
+ module.exports = ({ itemActions, utils }) =>
2
+ function getTotalPaidToRedistribute({ orders = [] }) {
3
+ const redistributableOrders = [];
4
+ let redistributableAmount = 0;
5
+
6
+ const appendIfRedistributable = currentOrder => {
7
+ const itemsTotalPaid = itemActions.getItemsTotalPaid({
8
+ orderItems: currentOrder.items || [],
9
+ });
10
+
11
+ const amount = utils.math.max(
12
+ utils.math.sub(currentOrder.totalPaid || 0, itemsTotalPaid),
13
+ 0
14
+ );
15
+
16
+ if (amount > 0) {
17
+ redistributableOrders.push({
18
+ ...currentOrder,
19
+ redistributableAmount: amount,
20
+ });
21
+ redistributableAmount = utils.math.add(redistributableAmount, amount);
22
+ }
23
+ };
24
+
25
+ (orders || []).forEach(order => {
26
+ if (!order || !order._id) return;
27
+ if (order.isParent) {
28
+ const childOrders = order.orders || [];
29
+ if (childOrders.length > 0) {
30
+ childOrders.forEach(appendIfRedistributable);
31
+ return;
32
+ }
33
+ }
34
+ appendIfRedistributable(order);
35
+ });
36
+
37
+ return {
38
+ redistributableOrders,
39
+ redistributableAmount,
40
+ };
41
+ };
@@ -82,6 +82,7 @@ const createSubOrder = require('./createSubOrder');
82
82
  const manualSplit = require('./manualSplit');
83
83
  const manualSplitByQuantity = require('./manualSplitByQuantity');
84
84
  const applyPayment = require('./applyPayment');
85
+ const applyRefund = require('./applyRefund');
85
86
  const getBalance = require('./getBalance');
86
87
  const getLastLocation = require('./getLastLocation');
87
88
  const getModifierRelations = require('./getModifierRelations');
@@ -101,6 +102,9 @@ const getTaxes = require('./getTaxes');
101
102
  const getPickedStatus = require('./getPickedStatus');
102
103
  const calculateWithPayment = require('./calculateWithPayment');
103
104
  const hasSerial = require('./hasSerial');
105
+ const getOverPaidAmount = require('./getOverPaidAmount');
106
+ const getTotalPaidToRedistribute = require('./getTotalPaidToRedistribute');
107
+ const refundOrderItem = require('./refundOrderItem');
104
108
 
105
109
  const orderActions = (deps = {}) => {
106
110
  const actions = {};
@@ -194,6 +198,7 @@ const orderActions = (deps = {}) => {
194
198
  manualSplit: manualSplit(innerDeps),
195
199
  manualSplitByQuantity: manualSplitByQuantity(innerDeps),
196
200
  applyPayment: applyPayment(innerDeps),
201
+ applyRefund: applyRefund(innerDeps),
197
202
  getOrdersBalance: getOrdersBalance(innerDeps),
198
203
  getLastLocation: getLastLocation(innerDeps),
199
204
  getModifierRelations: getModifierRelations(innerDeps),
@@ -213,6 +218,9 @@ const orderActions = (deps = {}) => {
213
218
  getPickedStatus: getPickedStatus(innerDeps),
214
219
  calculateWithPayment: calculateWithPayment(innerDeps),
215
220
  hasSerial: hasSerial(innerDeps),
221
+ getOverPaidAmount: getOverPaidAmount(innerDeps),
222
+ getTotalPaidToRedistribute: getTotalPaidToRedistribute(innerDeps),
223
+ refundOrderItem: refundOrderItem(innerDeps),
216
224
  });
217
225
 
218
226
  Object.keys(freezedActions).forEach(actionName => {
@@ -0,0 +1,29 @@
1
+ module.exports = ({ utils }) =>
2
+ function refundOrderItem({ item, orderId, amountToRefund = 0 }) {
3
+ if (amountToRefund <= 0) return { refunded: 0 };
4
+
5
+ const refundableAmount = item.totalPaid || 0;
6
+ const refunded = utils.math.min(amountToRefund, refundableAmount);
7
+ const amountToAssign = utils.math.sub(0, refunded);
8
+ const updatedTotalPaid = utils.math.add(
9
+ item.totalPaid || 0,
10
+ amountToAssign
11
+ );
12
+
13
+ return {
14
+ refunded,
15
+ item: {
16
+ ...item,
17
+ orderId,
18
+ amount: utils.math.add(item.amount || 0, amountToAssign),
19
+ totalPaid: updatedTotalPaid,
20
+ status: {
21
+ ...item.status,
22
+ paid: {
23
+ value: updatedTotalPaid >= (item.total || 0),
24
+ date: new Date(),
25
+ },
26
+ },
27
+ },
28
+ };
29
+ };
@@ -1,13 +1,20 @@
1
- module.exports = ({ actions }) =>
1
+ module.exports = ({ orderActions, utils }) =>
2
2
  function getOverPaidOrders({ orders = [] }) {
3
- let overPaidOrders = [];
3
+ const overPaidOrders = [];
4
4
  let overPaidAmount = 0;
5
5
 
6
6
  const appendIfOverPaid = currentOrder => {
7
- overPaidOrders = actions.appendIfOverPaid({
8
- currentOrder,
9
- overPaidOrders,
7
+ const overPaidTotal = orderActions.getOverPaidAmount({
8
+ order: currentOrder,
10
9
  });
10
+
11
+ if (overPaidTotal > 0) {
12
+ overPaidOrders.push({
13
+ ...currentOrder,
14
+ refundableAmount: overPaidTotal,
15
+ });
16
+ overPaidAmount = utils.math.add(overPaidAmount, overPaidTotal);
17
+ }
11
18
  };
12
19
 
13
20
  (orders || []).forEach(order => {
@@ -24,11 +31,6 @@ module.exports = ({ actions }) =>
24
31
  appendIfOverPaid(order);
25
32
  });
26
33
 
27
- overPaidAmount = overPaidOrders.reduce(
28
- (total, order) => total + Number(order.refundableAmount || 0),
29
- 0
30
- );
31
-
32
34
  return {
33
35
  overPaidOrders,
34
36
  overPaidAmount,
@@ -5,30 +5,17 @@ module.exports = ({ utils }) => {
5
5
  if (!order) return 0;
6
6
 
7
7
  const items = Array.isArray(order.items) ? order.items : [];
8
- const paidItems = items.filter(item => Number(item.totalPaid || 0) > 0);
8
+ const orderTotal =
9
+ typeof order.total === 'number'
10
+ ? order.total
11
+ : items.reduce(
12
+ (acc, item) => math.add(acc, Number(item.total || 0)),
13
+ 0
14
+ );
9
15
 
10
- const itemsTotalPaid = items.reduce(
11
- (acc, item) => math.add(acc, Number(item.totalPaid || 0)),
16
+ return math.max(
17
+ math.sub(Number(order.totalPaid || 0), Number(orderTotal || 0)),
12
18
  0
13
19
  );
14
- const paidItemsTotalPaid = paidItems.reduce(
15
- (acc, item) => math.add(acc, Number(item.totalPaid || 0)),
16
- 0
17
- );
18
- const paidItemsTotal = paidItems.reduce(
19
- (acc, item) => math.add(acc, Number(item.total || 0)),
20
- 0
21
- );
22
-
23
- const orphanedPaidAmount = math.max(
24
- math.sub(order.totalPaid || 0, itemsTotalPaid),
25
- 0
26
- );
27
- const refundablePaidItemsAmount = math.max(
28
- math.sub(paidItemsTotalPaid, paidItemsTotal),
29
- 0
30
- );
31
-
32
- return math.add(orphanedPaidAmount, refundablePaidItemsAmount);
33
20
  };
34
21
  };
@@ -1,9 +1,8 @@
1
1
  //
2
- const appendIfOverPaid = require('./appendIfOverPaid');
3
2
  const getMaxAmountToRefund = require('./getMaxAmountToRefund');
4
3
  const getRefundableOrderAmount = require('./getRefundableOrderAmount');
5
- const getOverPaidOrders = require('./getOverPaidOrders');
6
4
  const getMethodLabel = require('./getMethodLabel');
5
+ const getOverPaidOrders = require('./getOverPaidOrders');
7
6
 
8
7
  const orderActions = (deps = {}) => {
9
8
  const actions = {};
@@ -14,11 +13,10 @@ const orderActions = (deps = {}) => {
14
13
  };
15
14
 
16
15
  const freezedActions = Object.freeze({
17
- appendIfOverPaid: appendIfOverPaid(innerDeps),
18
16
  getMaxAmountToRefund: getMaxAmountToRefund(innerDeps),
19
17
  getRefundableOrderAmount: getRefundableOrderAmount(innerDeps),
20
- getOverPaidOrders: getOverPaidOrders(innerDeps),
21
18
  getMethodLabel: getMethodLabel(innerDeps),
19
+ getOverPaidOrders: getOverPaidOrders(innerDeps),
22
20
  });
23
21
 
24
22
  Object.keys(freezedActions).forEach(actionName => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@darkpos/pricing",
3
- "version": "1.0.152",
3
+ "version": "1.0.153",
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": "1dff1f50b45bf68bd10e8d8ef7fe18c2b37c751e"
57
+ "gitHead": "495fd0beca96603861fccf52ba2a5618fc632e7a"
58
58
  }
@@ -1,106 +0,0 @@
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
- });
@@ -1,59 +0,0 @@
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
- };
@@ -1,25 +0,0 @@
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 = orderItem ? orderItem.total : paymentOrderItem.total || 0;
11
- const itemTotalPaid = orderItem
12
- ? orderItem.totalPaid
13
- : 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
- };
@@ -1,17 +0,0 @@
1
- module.exports = ({ actions }) =>
2
- function appendIfOverPaid({ currentOrder, overPaidOrders = [] }) {
3
- if (!currentOrder || !currentOrder._id) return overPaidOrders;
4
-
5
- const refundableAmount = actions.getRefundableOrderAmount({
6
- order: currentOrder,
7
- });
8
- if (refundableAmount <= 0) return overPaidOrders;
9
-
10
- return [
11
- ...overPaidOrders,
12
- {
13
- ...currentOrder,
14
- refundableAmount,
15
- },
16
- ];
17
- };