@darkpos/pricing 1.0.151 → 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.
@@ -1,366 +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
- });
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('applyRefundToInvoices — 5-item order with 3 items priced to 0', () => {
201
- const ORDER_5_ID = 'order-5items';
202
-
203
- // Items as they appear in the payment invoice (stale totals from payment time)
204
- const shortsInvoiceItem = {
205
- orderId: ORDER_5_ID,
206
- orderItemId: 'shorts',
207
- total: 6.3,
208
- totalPaid: 6.3,
209
- amount: 6.3,
210
- status: { paid: { value: true } },
211
- };
212
- const jacket1InvoiceItem = {
213
- orderId: ORDER_5_ID,
214
- orderItemId: 'jacket-1',
215
- total: 8.93,
216
- totalPaid: 8.93,
217
- amount: 8.93,
218
- status: { paid: { value: true } },
219
- };
220
- const longJacketInvoiceItem = {
221
- orderId: ORDER_5_ID,
222
- orderItemId: 'long-jacket',
223
- total: 12.6,
224
- totalPaid: 12.6,
225
- amount: 12.6,
226
- status: { paid: { value: true } },
227
- };
228
- const jacket2InvoiceItem = {
229
- orderId: ORDER_5_ID,
230
- orderItemId: 'jacket-2',
231
- total: 8.93,
232
- totalPaid: 8.93,
233
- amount: 8.93,
234
- status: { paid: { value: true } },
235
- };
236
- const coatInvoiceItem = {
237
- orderId: ORDER_5_ID,
238
- orderItemId: 'coat',
239
- total: 13.65,
240
- totalPaid: 3.24,
241
- amount: 3.24,
242
- status: { paid: { value: false } },
243
- };
244
-
245
- // Live order state after Jacket×2 and Long Jacket prices changed to 0
246
- const liveOrder5 = {
247
- _id: ORDER_5_ID,
248
- items: [
249
- {
250
- _id: 'shorts',
251
- total: 6.3,
252
- totalPaid: 6.3,
253
- status: { paid: { value: true } },
254
- },
255
- {
256
- _id: 'jacket-1',
257
- total: 0,
258
- totalPaid: 8.93,
259
- status: { paid: { value: true } },
260
- },
261
- {
262
- _id: 'long-jacket',
263
- total: 0,
264
- totalPaid: 12.6,
265
- status: { paid: { value: true } },
266
- },
267
- {
268
- _id: 'jacket-2',
269
- total: 0,
270
- totalPaid: 8.93,
271
- status: { paid: { value: true } },
272
- },
273
- {
274
- _id: 'coat',
275
- total: 13.65,
276
- totalPaid: 3.24,
277
- status: { paid: { value: false } },
278
- },
279
- ],
280
- };
281
-
282
- const ordersById5 = { [ORDER_5_ID]: liveOrder5 };
283
-
284
- const paymentInvoices5 = [
285
- {
286
- invoiceId: 'inv-5items',
287
- amount: 40,
288
- paymentOrderItems: [
289
- shortsInvoiceItem,
290
- jacket1InvoiceItem,
291
- longJacketInvoiceItem,
292
- jacket2InvoiceItem,
293
- coatInvoiceItem,
294
- ],
295
- },
296
- ];
297
-
298
- test('correctly refunds 20.05 — clears the 3 zero-priced items and credits the remaining balance to coat', () => {
299
- const refundInvoices = pricing.invoice.applyRefundToInvoices(
300
- paymentInvoices5,
301
- ordersById5,
302
- 20.05
303
- );
304
-
305
- expect(refundInvoices).toHaveLength(1);
306
- expect(refundInvoices[0].amount).toBe(-20.05);
307
- expect(refundInvoices[0].paymentOrderItems).toHaveLength(4); // shorts has 0 excess, skipped
308
-
309
- const byItemId = Object.fromEntries(
310
- refundInvoices[0].paymentOrderItems.map(item => [item.orderItemId, item])
311
- );
312
-
313
- expect(byItemId['jacket-1'].amount).toBe(-8.93);
314
- expect(byItemId['jacket-1'].status.paid.value).toBe(true);
315
-
316
- expect(byItemId['long-jacket'].amount).toBe(-12.6);
317
- expect(byItemId['long-jacket'].status.paid.value).toBe(true);
318
-
319
- expect(byItemId['jacket-2'].amount).toBe(-8.93);
320
- expect(byItemId['jacket-2'].status.paid.value).toBe(true);
321
-
322
- expect(byItemId.coat.amount).toBe(10.41);
323
- expect(byItemId.coat.status.paid.value).toBe(true);
324
- });
325
- });
326
-
327
- describe('buildItemRefund', () => {
328
- const overpaidItemState = pricing.invoice.resolveItemState(
329
- overpaidInvoiceItem,
330
- ordersById
331
- );
332
- const underpaidItemState = pricing.invoice.resolveItemState(
333
- partiallyPaidInvoiceItem,
334
- ordersById
335
- );
336
-
337
- test('overpaid item: refunding its full excess results in a negative amount and paid = true', () => {
338
- const itemRefund = pricing.invoice.buildItemRefund(overpaidItemState, 4.2);
339
- expect(itemRefund.amount).toBe(-4.2);
340
- expect(itemRefund.totalPaid).toBe(14.7); // pre-refund value preserved
341
- expect(itemRefund.total).toBe(10.5);
342
- expect(itemRefund.status.paid.value).toBe(true); // 14.7 - 4.2 = 10.5 >= 10.5
343
- });
344
-
345
- test('underpaid item: applying a credit results in a positive amount and paid = true', () => {
346
- const itemRefund = pricing.invoice.buildItemRefund(
347
- underpaidItemState,
348
- -0.2
349
- );
350
- expect(itemRefund.amount).toBe(0.2); // positive = credit
351
- expect(itemRefund.totalPaid).toBe(10.3);
352
- expect(itemRefund.total).toBe(10.5);
353
- expect(itemRefund.status.paid.value).toBe(true); // 10.3 - (-0.2) = 10.5 >= 10.5
354
- });
355
-
356
- test('paid stays false when the refund does not fully cover the remaining balance', () => {
357
- const itemRefund = pricing.invoice.buildItemRefund(underpaidItemState, 2); // totalPaid drops to 8.3
358
- expect(itemRefund.status.paid.value).toBe(false); // 8.3 < 10.5
359
- });
360
-
361
- test('preserves all other fields from the original invoice item', () => {
362
- const itemRefund = pricing.invoice.buildItemRefund(overpaidItemState, 4.2);
363
- expect(itemRefund.orderId).toBe(ORDER_ID);
364
- expect(itemRefund.orderItemId).toBe(OVERPAID_ITEM_ID);
365
- });
366
- });
@@ -1,52 +0,0 @@
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
- };
@@ -1,23 +0,0 @@
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
- };
@@ -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,7 +0,0 @@
1
- module.exports = ({ utils }) =>
2
- function getOverpaidAmount({ item }) {
3
- const total = Number(item.total || 0);
4
- const totalPaid = Number(item.totalPaid || 0);
5
-
6
- return utils.math.sub(totalPaid, total);
7
- };
@@ -1,13 +0,0 @@
1
- module.exports = ({ utils }) =>
2
- function getOverPaidAmount({ order }) {
3
- if (!order) return 0;
4
-
5
- return (order.items || []).reduce((net, item) => {
6
- const itemTotalPaid = Number(item.totalPaid || 0);
7
- if (itemTotalPaid <= 0) return net;
8
- return utils.math.add(
9
- net,
10
- utils.math.sub(itemTotalPaid, Number(item.total || 0))
11
- );
12
- }, 0);
13
- };