@fire-spark/payments 0.3.5 → 0.4.1
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/dist/index.d.ts +42 -21
- package/dist/index.js +314 -211
- package/package.json +4 -2
package/dist/index.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { Big } from "big.js";
|
|
1
2
|
import { MixedPaymentsEffect, OrderLine, OrderTotals, PaymentCollectAt, PaymentCollectAt as PaymentCollectAt$1, PaymentErrorCode as PaymentErrorCode$1, PaymentIntent, PaymentMethod, PaymentProvider, PaymentScope, PaymentStatus, PaymentTender, TenderAllocation, TenderAllocationInput, TenderAmount, TenderDetails } from "@fire-spark/types";
|
|
2
3
|
//#region src/allocation.d.ts
|
|
3
4
|
/** `metadata.fee_based_on` values that are a tip (always primary, D18). */
|
|
@@ -9,7 +10,7 @@ type AllocateTenderInput = {
|
|
|
9
10
|
sequence: number;
|
|
10
11
|
method: PaymentMethod;
|
|
11
12
|
allocation: TenderAllocationInput | null;
|
|
12
|
-
/** Value a REDEMPTION tender covers (`details.
|
|
13
|
+
/** Value a REDEMPTION tender covers (`details.amount`). */
|
|
13
14
|
redemption_amount?: number;
|
|
14
15
|
};
|
|
15
16
|
type AllocateTendersInput = {
|
|
@@ -43,9 +44,9 @@ declare const isTipFee: (line: OrderLine) => boolean;
|
|
|
43
44
|
/** Delivery fee line (distance or coverage zone based). */
|
|
44
45
|
declare const isDeliveryFee: (line: OrderLine) => boolean;
|
|
45
46
|
/**
|
|
46
|
-
* Splits `amount`
|
|
47
|
-
*
|
|
48
|
-
*
|
|
47
|
+
* Splits `amount` by `units / quantity`. Parts are ordered by `sequence`.
|
|
48
|
+
* Every part but the last is `amount * units / quantity`. The last part is
|
|
49
|
+
* the remainder, so the parts sum to `amount`.
|
|
49
50
|
*/
|
|
50
51
|
declare const prorateMinor: (amount: number, quantity: number, parts: readonly {
|
|
51
52
|
units: number;
|
|
@@ -53,10 +54,10 @@ declare const prorateMinor: (amount: number, quantity: number, parts: readonly {
|
|
|
53
54
|
}[]) => number[];
|
|
54
55
|
/**
|
|
55
56
|
* Server-side money of every tender (§3.2): whole lines as stored, parts
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
*
|
|
59
|
-
*
|
|
57
|
+
* split exactly (last part is the remainder), fees to their tender, tip /
|
|
58
|
+
* delivery and any order-level difference to the primary, then REDEMPTION
|
|
59
|
+
* deducted from the primary first and by `sequence`, never below a tender's
|
|
60
|
+
* tip + delivery (D18). Amounts are not rounded.
|
|
60
61
|
*/
|
|
61
62
|
declare const allocateTenders: (input: AllocateTendersInput) => AllocateTendersResult;
|
|
62
63
|
/**
|
|
@@ -93,6 +94,27 @@ declare const validateTendersAgainstRules: (input: {
|
|
|
93
94
|
effect: MixedPaymentsEffect | null;
|
|
94
95
|
}) => ValidateTendersResult;
|
|
95
96
|
//#endregion
|
|
97
|
+
//#region src/money.d.ts
|
|
98
|
+
/** One monetary amount. Arithmetic stays on Big until a caller serializes. */
|
|
99
|
+
type Money = Big;
|
|
100
|
+
declare const money: (value: number | string | Money) => import("big.js").Big;
|
|
101
|
+
declare const moneyZero: () => import("big.js").Big;
|
|
102
|
+
declare const moneySum: (values: readonly Money[]) => Big;
|
|
103
|
+
declare const moneyMax: (left: Money, right: Money) => Big;
|
|
104
|
+
declare const moneyMin: (left: Money, right: Money) => Big;
|
|
105
|
+
declare const moneyNumber: (value: Money) => number;
|
|
106
|
+
/**
|
|
107
|
+
* Splits `whole` by `weights`. Every part but the last is
|
|
108
|
+
* `whole * weight / totalWeight`. The last part is what remains, so the
|
|
109
|
+
* parts add up to `whole`.
|
|
110
|
+
*/
|
|
111
|
+
declare const splitMoney: (whole: Money, weights: readonly Money[]) => Big[];
|
|
112
|
+
/**
|
|
113
|
+
* Serializes parts so a Big sum of the numbers equals `whole`. Earlier
|
|
114
|
+
* parts keep their value; the last part is `whole` minus those numbers.
|
|
115
|
+
*/
|
|
116
|
+
declare const moneyNumbers: (whole: Money, parts: readonly Money[]) => number[];
|
|
117
|
+
//#endregion
|
|
96
118
|
//#region src/errors.d.ts
|
|
97
119
|
type PaymentErrorReason = "timeout" | "network" | "declined" | "provider" | "ambiguous" | "user";
|
|
98
120
|
type ErrorDefaults = {
|
|
@@ -417,7 +439,8 @@ type PaymentAdapterCapabilities = {
|
|
|
417
439
|
/** ISO 4217 minor units; anything not listed uses 2. */
|
|
418
440
|
declare const CURRENCY_DECIMALS: Readonly<Record<string, number>>;
|
|
419
441
|
declare const currencyDecimals: (currency: string) => number;
|
|
420
|
-
|
|
442
|
+
/** Full precision. `decimals` is ignored so amounts are never truncated. */
|
|
443
|
+
declare const roundTo: (value: number, _decimals: number) => number;
|
|
421
444
|
declare const toPaymentAmount: (value: number, currency: string) => PaymentAmount;
|
|
422
445
|
type ChargeConversion = {
|
|
423
446
|
amount: PaymentAmount;
|
|
@@ -472,16 +495,14 @@ type RedemptionDetails = Extract<TenderDetails, {
|
|
|
472
495
|
/** One reward inside a grouped REDEMPTION tender (entry totals). */
|
|
473
496
|
type RedemptionReward = RedemptionDetails["rewards"][number];
|
|
474
497
|
/**
|
|
475
|
-
* Payment redeem as stored in `orders.redeem`. `amount`
|
|
476
|
-
*
|
|
498
|
+
* Payment redeem as stored in `orders.redeem`. `amount` is the total of
|
|
499
|
+
* the entry (every unit); `quantity` is informational.
|
|
477
500
|
*/
|
|
478
501
|
type PaymentRedeemLike = {
|
|
479
502
|
reward_id: string;
|
|
480
503
|
quantity?: number;
|
|
481
504
|
payment_id?: string | null;
|
|
482
505
|
amount?: number;
|
|
483
|
-
points?: number;
|
|
484
|
-
program?: string;
|
|
485
506
|
};
|
|
486
507
|
/** Payment redeems of one `payment_id`, ready to seed its tender. */
|
|
487
508
|
type RedemptionGroup = {
|
|
@@ -491,7 +512,7 @@ type RedemptionGroup = {
|
|
|
491
512
|
details: RedemptionDetails;
|
|
492
513
|
};
|
|
493
514
|
/**
|
|
494
|
-
*
|
|
515
|
+
* Reward (voucher, balance…) paid with a REDEMPTION tender, not a discount (D16):
|
|
495
516
|
* it never discounts lines nor has a `redemption_steps` entry.
|
|
496
517
|
*/
|
|
497
518
|
declare const isPaymentRedeem: (redeem: {
|
|
@@ -529,16 +550,16 @@ declare const paymentRedeemsCoverTotal: (redeem: readonly PaymentRedeemLike[], t
|
|
|
529
550
|
*/
|
|
530
551
|
declare const groupPaymentRedeems: (redeem: readonly PaymentRedeemLike[]) => RedemptionGroup[];
|
|
531
552
|
/**
|
|
532
|
-
* REDEMPTION `details` in the
|
|
533
|
-
*
|
|
553
|
+
* REDEMPTION `details` in the current shape, or null for other methods.
|
|
554
|
+
* Details persisted earlier (one `reward_id`, or money as `points_value`)
|
|
555
|
+
* read as the same shape; their old keys are dropped.
|
|
534
556
|
*/
|
|
535
557
|
declare const redemptionDetailsOf: (details: unknown) => RedemptionDetails | null;
|
|
536
558
|
/** Rewards of a REDEMPTION tender; empty for other methods. */
|
|
537
559
|
declare const redemptionRewardsOf: (details: unknown) => {
|
|
538
560
|
reward_id: string;
|
|
539
561
|
quantity: number;
|
|
540
|
-
|
|
541
|
-
points_value: number;
|
|
562
|
+
amount: number;
|
|
542
563
|
}[];
|
|
543
564
|
//#endregion
|
|
544
565
|
//#region src/reference.d.ts
|
|
@@ -624,7 +645,7 @@ declare const primaryTender: <T extends TenderLike>(intent: {
|
|
|
624
645
|
primary_tender_id: string | null;
|
|
625
646
|
tenders: readonly T[];
|
|
626
647
|
} | null | undefined) => T | null;
|
|
627
|
-
/** A REDEMPTION tender whose
|
|
648
|
+
/** A REDEMPTION tender whose reward is held for the order (L2 / D30). */
|
|
628
649
|
declare const isRedemptionReserved: (tender: TenderLike) => boolean;
|
|
629
650
|
/** Paid, or a reserved REDEMPTION (confirmed later, §3.3). */
|
|
630
651
|
declare const isTenderSettled: (tender: TenderLike) => boolean;
|
|
@@ -635,7 +656,7 @@ declare const isIntentCovered: (intent: IntentLike | null | undefined, orderTota
|
|
|
635
656
|
/**
|
|
636
657
|
* Order `payment_status` from the live tenders of the current intent
|
|
637
658
|
* (D32). Reserved REDEMPTION tenders count as settled, so a redemption-only
|
|
638
|
-
* intent is `PAID` once its
|
|
659
|
+
* intent is `PAID` once its rewards are held (D30).
|
|
639
660
|
*/
|
|
640
661
|
declare const derivePaymentStatus: (intent: IntentLike | null | undefined) => PaymentStatus;
|
|
641
662
|
/** Live tenders a `PAYING` node waits for (§7.3); no scope = all. */
|
|
@@ -702,4 +723,4 @@ declare const scopedPaymentStorage: (storage: PaymentStorage, paymentId: string)
|
|
|
702
723
|
/** For tests and web previews; apps provide AsyncStorage or localStorage. */
|
|
703
724
|
declare const memoryPaymentStorage: () => PaymentStorage;
|
|
704
725
|
//#endregion
|
|
705
|
-
export { ACTIVE_INTENT_STATUSES, AllocateTenderInput, AllocateTendersInput, AllocateTendersResult, AllocatedTender, AllocationError, CURRENCY_DECIMALS, ChargeConversion, DEFAULT_PAYMENT_POLICY, DELIVERY_FEE_BASED_ON, DIGITAL_COLLECT_AT, type DeviceField, MergePaymentTenderOutcome, MergePaymentTenderResult, NEXT_STATUSES, PARTIAL_APPROVAL_ERROR, PAYMENT_ERROR_CODES, PAYMENT_ERROR_DEFAULTS, type PaymentAction, type PaymentAdapter, type PaymentAdapterCapabilities, type PaymentAmount, type PaymentCardType, type PaymentCollectAt, type PaymentContext, type PaymentCustomer, type PaymentEntryMode, type PaymentEnvironment, PaymentError, PaymentErrorCode, PaymentErrorDictionary, PaymentErrorEntry, PaymentErrorReason, type PaymentIntentStatus, type PaymentMetadata, type PaymentMetadataPayment, type PaymentOperation, type PaymentPolicy, PaymentRedeemLike, PaymentRedeemSource, type PaymentResult, type PaymentReversal, type PaymentServer, type PaymentStorage, PaymentTenderUpdate, type PaymentUI, REDEMPTION_METHOD, RawPaymentError, RedemptionDetails, RedemptionGroup, RedemptionReward, RulesTender, StandardPaymentError, TERMINAL_TENDER_STATUSES, TIP_FEE_BASED_ON, type TaxBreakdown, TaxLine, type TerminalContext, type TerminalOperation, type TerminalOperations, type TerminalResult, ValidateTendersResult, activeIntent, allocateTenders, amountTolerance, batchCloseDay, countTenderItems, createAttemptReference, createPaymentError, currencyDecimals, currentIntent, defaultReversalFor, definePaymentErrors, derivePaymentStatus, discountRedeems, formatPaymentError, groupPaymentRedeems, isBatchCloseDue, isDeliveryFee, isIntentCovered, isMixedTenders, isPaymentError, isPaymentRedeem, isRealPaymentMethod, isRedemptionReserved, isScopePaid, isTenderSettled, isTerminalLocked, isTipFee, liveRealTenders, liveTenders, memoryPaymentStorage, mergePaymentTender, missingDeviceFields, nextIntentStatus, orderPaymentRedeems, paymentRedeemsCoverTotal, primaryTender, primaryTenderOf, prorateMinor, realTenders, redemptionDetailsOf, redemptionRewardsOf, resolvePaymentPolicy, resolveProviderEnvironment, roundTo, scopedPaymentStorage, taxBreakdown, tendersCoverage, tendersInScope, toChargeAmount, toPaymentAmount, toPaymentError, validateTendersAgainstRules, withTerminalLock };
|
|
726
|
+
export { ACTIVE_INTENT_STATUSES, AllocateTenderInput, AllocateTendersInput, AllocateTendersResult, AllocatedTender, AllocationError, CURRENCY_DECIMALS, ChargeConversion, DEFAULT_PAYMENT_POLICY, DELIVERY_FEE_BASED_ON, DIGITAL_COLLECT_AT, type DeviceField, MergePaymentTenderOutcome, MergePaymentTenderResult, Money, NEXT_STATUSES, PARTIAL_APPROVAL_ERROR, PAYMENT_ERROR_CODES, PAYMENT_ERROR_DEFAULTS, type PaymentAction, type PaymentAdapter, type PaymentAdapterCapabilities, type PaymentAmount, type PaymentCardType, type PaymentCollectAt, type PaymentContext, type PaymentCustomer, type PaymentEntryMode, type PaymentEnvironment, PaymentError, PaymentErrorCode, PaymentErrorDictionary, PaymentErrorEntry, PaymentErrorReason, type PaymentIntentStatus, type PaymentMetadata, type PaymentMetadataPayment, type PaymentOperation, type PaymentPolicy, PaymentRedeemLike, PaymentRedeemSource, type PaymentResult, type PaymentReversal, type PaymentServer, type PaymentStorage, PaymentTenderUpdate, type PaymentUI, REDEMPTION_METHOD, RawPaymentError, RedemptionDetails, RedemptionGroup, RedemptionReward, RulesTender, StandardPaymentError, TERMINAL_TENDER_STATUSES, TIP_FEE_BASED_ON, type TaxBreakdown, TaxLine, type TerminalContext, type TerminalOperation, type TerminalOperations, type TerminalResult, ValidateTendersResult, activeIntent, allocateTenders, amountTolerance, batchCloseDay, countTenderItems, createAttemptReference, createPaymentError, currencyDecimals, currentIntent, defaultReversalFor, definePaymentErrors, derivePaymentStatus, discountRedeems, formatPaymentError, groupPaymentRedeems, isBatchCloseDue, isDeliveryFee, isIntentCovered, isMixedTenders, isPaymentError, isPaymentRedeem, isRealPaymentMethod, isRedemptionReserved, isScopePaid, isTenderSettled, isTerminalLocked, isTipFee, liveRealTenders, liveTenders, memoryPaymentStorage, mergePaymentTender, missingDeviceFields, money, moneyMax, moneyMin, moneyNumber, moneyNumbers, moneySum, moneyZero, nextIntentStatus, orderPaymentRedeems, paymentRedeemsCoverTotal, primaryTender, primaryTenderOf, prorateMinor, realTenders, redemptionDetailsOf, redemptionRewardsOf, resolvePaymentPolicy, resolveProviderEnvironment, roundTo, scopedPaymentStorage, splitMoney, taxBreakdown, tendersCoverage, tendersInScope, toChargeAmount, toPaymentAmount, toPaymentError, validateTendersAgainstRules, withTerminalLock };
|
package/dist/index.js
CHANGED
|
@@ -1,3 +1,47 @@
|
|
|
1
|
+
import { Big } from "big.js";
|
|
2
|
+
//#region src/money.ts
|
|
3
|
+
const money = (value) => {
|
|
4
|
+
return new Big(value);
|
|
5
|
+
};
|
|
6
|
+
const moneyZero = () => new Big(0);
|
|
7
|
+
const moneySum = (values) => {
|
|
8
|
+
return values.reduce((sum, value) => sum.plus(value), moneyZero());
|
|
9
|
+
};
|
|
10
|
+
const moneyMax = (left, right) => {
|
|
11
|
+
return left.gt(right) ? left : right;
|
|
12
|
+
};
|
|
13
|
+
const moneyMin = (left, right) => {
|
|
14
|
+
return left.lt(right) ? left : right;
|
|
15
|
+
};
|
|
16
|
+
const moneyNumber = (value) => value.toNumber();
|
|
17
|
+
/**
|
|
18
|
+
* Splits `whole` by `weights`. Every part but the last is
|
|
19
|
+
* `whole * weight / totalWeight`. The last part is what remains, so the
|
|
20
|
+
* parts add up to `whole`.
|
|
21
|
+
*/
|
|
22
|
+
const splitMoney = (whole, weights) => {
|
|
23
|
+
if (weights.length === 0) return [];
|
|
24
|
+
const total = moneySum(weights);
|
|
25
|
+
if (total.eq(0)) return weights.map(() => moneyZero());
|
|
26
|
+
let rest = whole;
|
|
27
|
+
return weights.map((weight, index) => {
|
|
28
|
+
if (index === weights.length - 1) return rest;
|
|
29
|
+
const share = whole.times(weight).div(total);
|
|
30
|
+
rest = rest.minus(share);
|
|
31
|
+
return share;
|
|
32
|
+
});
|
|
33
|
+
};
|
|
34
|
+
/**
|
|
35
|
+
* Serializes parts so a Big sum of the numbers equals `whole`. Earlier
|
|
36
|
+
* parts keep their value; the last part is `whole` minus those numbers.
|
|
37
|
+
*/
|
|
38
|
+
const moneyNumbers = (whole, parts) => {
|
|
39
|
+
if (parts.length === 0) return [];
|
|
40
|
+
const head = parts.slice(0, -1).map((part) => part.toNumber());
|
|
41
|
+
const used = head.reduce((sum, value) => sum.plus(value), moneyZero());
|
|
42
|
+
return [...head, whole.minus(used).toNumber()];
|
|
43
|
+
};
|
|
44
|
+
//#endregion
|
|
1
45
|
//#region src/amount.ts
|
|
2
46
|
/** ISO 4217 minor units; anything not listed uses 2. */
|
|
3
47
|
const CURRENCY_DECIMALS = {
|
|
@@ -16,17 +60,19 @@ const CURRENCY_DECIMALS = {
|
|
|
16
60
|
const currencyDecimals = (currency) => {
|
|
17
61
|
return CURRENCY_DECIMALS[currency.toUpperCase()] ?? 2;
|
|
18
62
|
};
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
return
|
|
63
|
+
/** Full precision. `decimals` is ignored so amounts are never truncated. */
|
|
64
|
+
const roundTo = (value, _decimals) => {
|
|
65
|
+
return moneyNumber(money(value));
|
|
22
66
|
};
|
|
23
67
|
const toPaymentAmount = (value, currency) => {
|
|
24
68
|
const code = currency.toUpperCase();
|
|
25
69
|
const decimals = currencyDecimals(code);
|
|
26
|
-
const
|
|
70
|
+
const exact = money(value);
|
|
71
|
+
const scale = money(10).pow(decimals);
|
|
72
|
+
const minor = moneyNumber(exact.times(scale));
|
|
27
73
|
return {
|
|
28
|
-
value:
|
|
29
|
-
minor
|
|
74
|
+
value: moneyNumber(exact),
|
|
75
|
+
minor,
|
|
30
76
|
currency: code,
|
|
31
77
|
decimals
|
|
32
78
|
};
|
|
@@ -41,7 +87,8 @@ const toChargeAmount = (input) => {
|
|
|
41
87
|
throw new Error(message);
|
|
42
88
|
}
|
|
43
89
|
const rate = same ? 1 : input.rate;
|
|
44
|
-
const
|
|
90
|
+
const charged = money(input.value).times(rate);
|
|
91
|
+
const amount = toPaymentAmount(moneyNumber(charged), chargeCurrency);
|
|
45
92
|
return {
|
|
46
93
|
amount,
|
|
47
94
|
charged_amount: amount.value,
|
|
@@ -137,7 +184,7 @@ const primaryTender = (intent) => {
|
|
|
137
184
|
const id = intent.primary_tender_id;
|
|
138
185
|
return intent.tenders.find((tender) => tender.id === id) ?? null;
|
|
139
186
|
};
|
|
140
|
-
/** A REDEMPTION tender whose
|
|
187
|
+
/** A REDEMPTION tender whose reward is held for the order (L2 / D30). */
|
|
141
188
|
const isRedemptionReserved = (tender) => {
|
|
142
189
|
if (isRealPaymentMethod(tender.method)) return false;
|
|
143
190
|
return typeof tender.metadata?.reserved_at === "string";
|
|
@@ -167,7 +214,7 @@ const isIntentCovered = (intent, orderTotal) => {
|
|
|
167
214
|
/**
|
|
168
215
|
* Order `payment_status` from the live tenders of the current intent
|
|
169
216
|
* (D32). Reserved REDEMPTION tenders count as settled, so a redemption-only
|
|
170
|
-
* intent is `PAID` once its
|
|
217
|
+
* intent is `PAID` once its rewards are held (D30).
|
|
171
218
|
*/
|
|
172
219
|
const derivePaymentStatus = (intent) => {
|
|
173
220
|
const tenders = liveTenders(intent);
|
|
@@ -370,18 +417,20 @@ const DELIVERY_FEE_BASED_ON = [
|
|
|
370
417
|
"DISTANCE_STREET",
|
|
371
418
|
"COVERAGE_ZONE_TAG"
|
|
372
419
|
];
|
|
373
|
-
const zeroMinor = () =>
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
420
|
+
const zeroMinor = () => {
|
|
421
|
+
return {
|
|
422
|
+
subtotal: moneyZero(),
|
|
423
|
+
discount: moneyZero(),
|
|
424
|
+
tax: moneyZero(),
|
|
425
|
+
total: moneyZero()
|
|
426
|
+
};
|
|
427
|
+
};
|
|
379
428
|
const addMinor = (left, right) => {
|
|
380
429
|
return {
|
|
381
|
-
subtotal: left.subtotal
|
|
382
|
-
discount: left.discount
|
|
383
|
-
tax: left.tax
|
|
384
|
-
total: left.total
|
|
430
|
+
subtotal: left.subtotal.plus(right.subtotal),
|
|
431
|
+
discount: left.discount.plus(right.discount),
|
|
432
|
+
tax: left.tax.plus(right.tax),
|
|
433
|
+
total: left.total.plus(right.total)
|
|
385
434
|
};
|
|
386
435
|
};
|
|
387
436
|
const feeBasedOf = (line) => {
|
|
@@ -423,43 +472,30 @@ const descendantsOf = (lines, rootUid) => {
|
|
|
423
472
|
return out;
|
|
424
473
|
};
|
|
425
474
|
/**
|
|
426
|
-
* Splits `amount`
|
|
427
|
-
*
|
|
428
|
-
*
|
|
475
|
+
* Splits `amount` by `units / quantity`. Parts are ordered by `sequence`.
|
|
476
|
+
* Every part but the last is `amount * units / quantity`. The last part is
|
|
477
|
+
* the remainder, so the parts sum to `amount`.
|
|
429
478
|
*/
|
|
430
479
|
const prorateMinor = (amount, quantity, parts) => {
|
|
431
|
-
const
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
480
|
+
const ordered = parts.map((part, index) => ({
|
|
481
|
+
...part,
|
|
482
|
+
index
|
|
483
|
+
})).toSorted((left, right) => left.sequence - right.sequence);
|
|
484
|
+
const whole = money(amount);
|
|
485
|
+
const qty = money(quantity);
|
|
486
|
+
let rest = whole;
|
|
487
|
+
const numbers = moneyNumbers(whole, ordered.map((part, index) => {
|
|
488
|
+
if (index === ordered.length - 1) return rest;
|
|
489
|
+
const share = whole.times(part.units).div(qty);
|
|
490
|
+
rest = rest.minus(share);
|
|
491
|
+
return share;
|
|
492
|
+
}));
|
|
493
|
+
const out = parts.map(() => 0);
|
|
494
|
+
ordered.forEach((part, index) => {
|
|
495
|
+
out[part.index] = numbers[index] ?? 0;
|
|
440
496
|
});
|
|
441
|
-
let residual = amount - shares.reduce((sum, share) => sum + share.base, 0);
|
|
442
|
-
const order = shares.toSorted((a, b) => {
|
|
443
|
-
if (b.remainder !== a.remainder) return b.remainder - a.remainder;
|
|
444
|
-
return a.sequence - b.sequence;
|
|
445
|
-
});
|
|
446
|
-
const out = shares.map((share) => share.base);
|
|
447
|
-
for (const share of order) {
|
|
448
|
-
if (residual <= 0) break;
|
|
449
|
-
out[share.index] = (out[share.index] ?? 0) + 1;
|
|
450
|
-
residual -= 1;
|
|
451
|
-
}
|
|
452
497
|
return out;
|
|
453
498
|
};
|
|
454
|
-
const createConverter = (currency) => {
|
|
455
|
-
const factor = 10 ** currencyDecimals(currency);
|
|
456
|
-
const toMinor = (value) => Math.round(value * factor);
|
|
457
|
-
const fromMinor = (value) => value / factor;
|
|
458
|
-
return {
|
|
459
|
-
toMinor,
|
|
460
|
-
fromMinor
|
|
461
|
-
};
|
|
462
|
-
};
|
|
463
499
|
const planAllocations = (input, real, roots) => {
|
|
464
500
|
const byUid = new Map(input.lines.map((line) => [line.uid, line]));
|
|
465
501
|
const plan = {
|
|
@@ -550,39 +586,39 @@ const planAllocations = (input, real, roots) => {
|
|
|
550
586
|
}
|
|
551
587
|
return plan;
|
|
552
588
|
};
|
|
553
|
-
const
|
|
589
|
+
const ownMoney = (line) => {
|
|
554
590
|
return {
|
|
555
|
-
subtotal:
|
|
556
|
-
discount:
|
|
557
|
-
tax:
|
|
558
|
-
total:
|
|
591
|
+
subtotal: money(line.subtotal),
|
|
592
|
+
discount: money(line.discount_total),
|
|
593
|
+
tax: money(line.tax_total),
|
|
594
|
+
total: money(line.total)
|
|
559
595
|
};
|
|
560
596
|
};
|
|
561
|
-
const
|
|
597
|
+
const groupMoney = (line) => {
|
|
562
598
|
return {
|
|
563
|
-
subtotal:
|
|
564
|
-
discount:
|
|
565
|
-
tax:
|
|
566
|
-
total:
|
|
599
|
+
subtotal: money(line.group_subtotal),
|
|
600
|
+
discount: money(line.group_discount),
|
|
601
|
+
tax: money(line.group_tax_total),
|
|
602
|
+
total: money(line.group_total)
|
|
567
603
|
};
|
|
568
604
|
};
|
|
569
605
|
const isConsistent = (value) => {
|
|
570
|
-
return
|
|
606
|
+
return value.subtotal.minus(value.discount).plus(value.tax).eq(value.total);
|
|
571
607
|
};
|
|
572
|
-
/** Split of one component per part
|
|
608
|
+
/** Split of one component per part. Each component sums to its source. */
|
|
573
609
|
const splitComponent = (value, quantity, parts) => {
|
|
574
|
-
const subtotal = prorateMinor(value.subtotal, quantity, parts);
|
|
575
|
-
const discount = prorateMinor(value.discount, quantity, parts);
|
|
576
|
-
const total = prorateMinor(value.total, quantity, parts);
|
|
610
|
+
const subtotal = prorateMinor(moneyNumber(value.subtotal), quantity, parts);
|
|
611
|
+
const discount = prorateMinor(moneyNumber(value.discount), quantity, parts);
|
|
612
|
+
const total = prorateMinor(moneyNumber(value.total), quantity, parts);
|
|
577
613
|
return parts.map((_, index) => {
|
|
578
|
-
const
|
|
579
|
-
const
|
|
580
|
-
const
|
|
614
|
+
const partSubtotal = money(subtotal[index] ?? 0);
|
|
615
|
+
const partDiscount = money(discount[index] ?? 0);
|
|
616
|
+
const partTotal = money(total[index] ?? 0);
|
|
581
617
|
return {
|
|
582
|
-
subtotal:
|
|
583
|
-
discount:
|
|
584
|
-
tax:
|
|
585
|
-
total:
|
|
618
|
+
subtotal: partSubtotal,
|
|
619
|
+
discount: partDiscount,
|
|
620
|
+
tax: partTotal.minus(partSubtotal).plus(partDiscount),
|
|
621
|
+
total: partTotal
|
|
586
622
|
};
|
|
587
623
|
});
|
|
588
624
|
};
|
|
@@ -593,6 +629,22 @@ const base = (tender) => {
|
|
|
593
629
|
method: tender.method
|
|
594
630
|
};
|
|
595
631
|
};
|
|
632
|
+
const balanceAmounts = (amounts, gross) => {
|
|
633
|
+
if (amounts.length === 0) return amounts;
|
|
634
|
+
const head = amounts.slice(0, -1);
|
|
635
|
+
const used = head.reduce((sum, row) => sum.plus(row.total), moneyZero());
|
|
636
|
+
const lastTotal = gross.minus(used);
|
|
637
|
+
const last = amounts[amounts.length - 1];
|
|
638
|
+
const drift = lastTotal.minus(last.total);
|
|
639
|
+
const subtotal = money(last.subtotal).plus(drift);
|
|
640
|
+
const total = moneyNumber(lastTotal);
|
|
641
|
+
const next = {
|
|
642
|
+
...last,
|
|
643
|
+
subtotal: moneyNumber(subtotal),
|
|
644
|
+
total
|
|
645
|
+
};
|
|
646
|
+
return [...head, next];
|
|
647
|
+
};
|
|
596
648
|
const emptyAmount = (currency, total) => {
|
|
597
649
|
const b1 = {
|
|
598
650
|
currency,
|
|
@@ -614,29 +666,30 @@ const emptyAmount = (currency, total) => {
|
|
|
614
666
|
};
|
|
615
667
|
/**
|
|
616
668
|
* Server-side money of every tender (§3.2): whole lines as stored, parts
|
|
617
|
-
*
|
|
618
|
-
*
|
|
619
|
-
*
|
|
620
|
-
*
|
|
669
|
+
* split exactly (last part is the remainder), fees to their tender, tip /
|
|
670
|
+
* delivery and any order-level difference to the primary, then REDEMPTION
|
|
671
|
+
* deducted from the primary first and by `sequence`, never below a tender's
|
|
672
|
+
* tip + delivery (D18). Amounts are not rounded.
|
|
621
673
|
*/
|
|
622
674
|
const allocateTenders = (input) => {
|
|
623
675
|
const currency = input.totals.currency;
|
|
624
|
-
const { toMinor, fromMinor } = createConverter(currency);
|
|
625
676
|
const sorted = input.tenders.toSorted((a, b) => a.sequence - b.sequence);
|
|
626
677
|
const real = sorted.filter((tender) => isRealPaymentMethod(tender.method));
|
|
627
678
|
const redemptions = sorted.filter((tender) => {
|
|
628
679
|
return !isRealPaymentMethod(tender.method);
|
|
629
680
|
});
|
|
630
681
|
const roots = input.lines.filter((line) => line.parent_uid === null);
|
|
631
|
-
const orderTotal =
|
|
682
|
+
const orderTotal = money(input.totals.total);
|
|
632
683
|
const tipDelivery = roots.reduce((sum, line) => {
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
684
|
+
if (!(isTipFee(line) || isDeliveryFee(line))) return sum;
|
|
685
|
+
return sum.plus(line.group_total);
|
|
686
|
+
}, moneyZero());
|
|
687
|
+
const redemptionValues = redemptions.map((tender) => {
|
|
688
|
+
return money(tender.redemption_amount ?? 0);
|
|
637
689
|
});
|
|
638
|
-
const redeemed =
|
|
639
|
-
|
|
690
|
+
const redeemed = moneySum(redemptionValues);
|
|
691
|
+
const cap = orderTotal.minus(tipDelivery);
|
|
692
|
+
if (redeemed.gt(cap)) return {
|
|
640
693
|
ok: false,
|
|
641
694
|
error: {
|
|
642
695
|
code: "PAYMENT_REDEMPTION_EXCEEDED",
|
|
@@ -653,7 +706,7 @@ const allocateTenders = (input) => {
|
|
|
653
706
|
}
|
|
654
707
|
};
|
|
655
708
|
const redemptionTenders = redemptions.map((tender, index) => {
|
|
656
|
-
const value =
|
|
709
|
+
const value = moneyNumber(redemptionValues[index] ?? moneyZero());
|
|
657
710
|
const amount = emptyAmount(currency, value);
|
|
658
711
|
return {
|
|
659
712
|
...base(tender),
|
|
@@ -662,10 +715,10 @@ const allocateTenders = (input) => {
|
|
|
662
715
|
};
|
|
663
716
|
});
|
|
664
717
|
if (real.length === 0) {
|
|
665
|
-
if (redeemed
|
|
718
|
+
if (!redeemed.eq(orderTotal)) {
|
|
666
719
|
const msg = "Redemptions must cover the order without a real tender";
|
|
667
720
|
let code = "PAYMENT_UNALLOCATED_LINES";
|
|
668
|
-
if (redeemed
|
|
721
|
+
if (redeemed.gt(orderTotal)) code = "PAYMENT_REDEMPTION_EXCEEDED";
|
|
669
722
|
return {
|
|
670
723
|
ok: false,
|
|
671
724
|
error: {
|
|
@@ -686,20 +739,27 @@ const allocateTenders = (input) => {
|
|
|
686
739
|
error: plan.fail
|
|
687
740
|
};
|
|
688
741
|
const builds = real.map(() => {
|
|
689
|
-
|
|
742
|
+
const b1 = {
|
|
690
743
|
products: zeroMinor(),
|
|
691
|
-
fees:
|
|
692
|
-
|
|
693
|
-
|
|
744
|
+
fees: moneyZero()
|
|
745
|
+
};
|
|
746
|
+
const b2 = {
|
|
747
|
+
tip: moneyZero(),
|
|
748
|
+
delivery: moneyZero(),
|
|
694
749
|
amounts: []
|
|
695
750
|
};
|
|
751
|
+
return {
|
|
752
|
+
...b1,
|
|
753
|
+
...b2
|
|
754
|
+
};
|
|
696
755
|
});
|
|
697
756
|
const toAmount = (uid, value, units) => {
|
|
698
|
-
const subtotal =
|
|
699
|
-
const discount =
|
|
757
|
+
const subtotal = moneyNumber(value.subtotal);
|
|
758
|
+
const discount = moneyNumber(value.discount);
|
|
759
|
+
const total = moneyNumber(value.total);
|
|
700
760
|
const b1 = {
|
|
701
|
-
tax:
|
|
702
|
-
total
|
|
761
|
+
tax: moneyNumber(money(total).minus(subtotal).plus(discount)),
|
|
762
|
+
total
|
|
703
763
|
};
|
|
704
764
|
return {
|
|
705
765
|
line_uid: uid,
|
|
@@ -712,11 +772,11 @@ const allocateTenders = (input) => {
|
|
|
712
772
|
for (const root of roots) {
|
|
713
773
|
if (root.type === "FEE") {
|
|
714
774
|
const build = builds[plan.fees.get(root.uid) ?? 0];
|
|
715
|
-
const total =
|
|
716
|
-
if (isTipFee(root)) build.tip
|
|
717
|
-
else if (isDeliveryFee(root)) build.delivery
|
|
718
|
-
else build.fees
|
|
719
|
-
const amount = toAmount(root.uid,
|
|
775
|
+
const total = money(root.group_total);
|
|
776
|
+
if (isTipFee(root)) build.tip = build.tip.plus(total);
|
|
777
|
+
else if (isDeliveryFee(root)) build.delivery = build.delivery.plus(total);
|
|
778
|
+
else build.fees = build.fees.plus(total);
|
|
779
|
+
const amount = toAmount(root.uid, groupMoney(root));
|
|
720
780
|
build.amounts.push({
|
|
721
781
|
...amount,
|
|
722
782
|
children: []
|
|
@@ -727,10 +787,10 @@ const allocateTenders = (input) => {
|
|
|
727
787
|
const wholeIndex = plan.whole.get(root.uid);
|
|
728
788
|
if (wholeIndex !== void 0) {
|
|
729
789
|
const build = builds[wholeIndex];
|
|
730
|
-
const group =
|
|
790
|
+
const group = groupMoney(root);
|
|
731
791
|
build.products = addMinor(build.products, group);
|
|
732
792
|
const childAmounts = children.map((child) => {
|
|
733
|
-
const own =
|
|
793
|
+
const own = ownMoney(child);
|
|
734
794
|
return {
|
|
735
795
|
...toAmount(child.uid, own),
|
|
736
796
|
quantity: child.quantity
|
|
@@ -747,7 +807,7 @@ const allocateTenders = (input) => {
|
|
|
747
807
|
const components = [root, ...children];
|
|
748
808
|
const splits = [];
|
|
749
809
|
for (const component of components) {
|
|
750
|
-
const own =
|
|
810
|
+
const own = ownMoney(component);
|
|
751
811
|
if (!isConsistent(own)) return {
|
|
752
812
|
ok: false,
|
|
753
813
|
error: {
|
|
@@ -765,7 +825,7 @@ const allocateTenders = (input) => {
|
|
|
765
825
|
build.products = addMinor(build.products, value);
|
|
766
826
|
const childAmounts = children.map((child, childIndex) => {
|
|
767
827
|
const share = shares[childIndex + 1] ?? zeroMinor();
|
|
768
|
-
const quantity = child.quantity
|
|
828
|
+
const quantity = moneyNumber(money(child.quantity).times(part.units).div(root.quantity));
|
|
769
829
|
return {
|
|
770
830
|
...toAmount(child.uid, share),
|
|
771
831
|
quantity
|
|
@@ -779,23 +839,25 @@ const allocateTenders = (input) => {
|
|
|
779
839
|
}
|
|
780
840
|
}
|
|
781
841
|
const allocated = builds.reduce((sum, build) => {
|
|
782
|
-
|
|
783
|
-
|
|
842
|
+
const gross = build.products.total.plus(build.fees);
|
|
843
|
+
return sum.plus(gross).plus(build.tip).plus(build.delivery);
|
|
844
|
+
}, moneyZero());
|
|
784
845
|
const primary = builds[0];
|
|
785
|
-
const adjustment = orderTotal
|
|
786
|
-
if (adjustment
|
|
846
|
+
const adjustment = orderTotal.minus(allocated);
|
|
847
|
+
if (adjustment.lt(0)) {
|
|
787
848
|
const value = { ...primary.products };
|
|
788
|
-
value.discount
|
|
789
|
-
value.total
|
|
849
|
+
value.discount = value.discount.minus(adjustment);
|
|
850
|
+
value.total = value.total.plus(adjustment);
|
|
790
851
|
primary.products = value;
|
|
791
|
-
} else if (adjustment
|
|
852
|
+
} else if (adjustment.gt(0)) primary.products = {
|
|
792
853
|
...primary.products,
|
|
793
|
-
|
|
854
|
+
subtotal: primary.products.subtotal.plus(adjustment),
|
|
855
|
+
total: primary.products.total.plus(adjustment)
|
|
794
856
|
};
|
|
795
857
|
const totals = builds.map((build) => {
|
|
796
|
-
return build.products.total
|
|
858
|
+
return build.products.total.plus(build.fees).plus(build.tip).plus(build.delivery);
|
|
797
859
|
});
|
|
798
|
-
if ((totals[0] ??
|
|
860
|
+
if ((totals[0] ?? moneyZero()).lt(0)) return {
|
|
799
861
|
ok: false,
|
|
800
862
|
error: {
|
|
801
863
|
code: "PAYMENT_INVALID_PRIMARY",
|
|
@@ -803,67 +865,104 @@ const allocateTenders = (input) => {
|
|
|
803
865
|
tender_id: real[0]?.id
|
|
804
866
|
}
|
|
805
867
|
};
|
|
806
|
-
const applied = builds.map(() =>
|
|
868
|
+
const applied = builds.map(() => moneyZero());
|
|
807
869
|
let pending = redeemed;
|
|
808
870
|
for (const [index, build] of builds.entries()) {
|
|
809
|
-
if (pending
|
|
810
|
-
const floor = build.tip
|
|
811
|
-
const
|
|
812
|
-
const take =
|
|
871
|
+
if (pending.lte(0)) break;
|
|
872
|
+
const floor = build.tip.plus(build.delivery);
|
|
873
|
+
const gross = totals[index] ?? moneyZero();
|
|
874
|
+
const take = moneyMin(moneyMax(moneyZero(), gross.minus(floor)), pending);
|
|
813
875
|
applied[index] = take;
|
|
814
|
-
pending
|
|
876
|
+
pending = pending.minus(take);
|
|
815
877
|
}
|
|
816
|
-
if (pending
|
|
878
|
+
if (pending.gt(0)) return {
|
|
817
879
|
ok: false,
|
|
818
880
|
error: {
|
|
819
881
|
code: "PAYMENT_REDEMPTION_EXCEEDED",
|
|
820
882
|
message: "Redemptions exceed what real tenders can absorb"
|
|
821
883
|
}
|
|
822
884
|
};
|
|
885
|
+
const tenders = [...real.map((tender, index) => {
|
|
886
|
+
const build = builds[index];
|
|
887
|
+
const subtotal = moneyNumber(build.products.subtotal);
|
|
888
|
+
const discount = moneyNumber(build.products.discount);
|
|
889
|
+
const tax = moneyNumber(build.products.tax);
|
|
890
|
+
const fees = moneyNumber(build.fees);
|
|
891
|
+
const tip = moneyNumber(build.tip);
|
|
892
|
+
const delivery = moneyNumber(build.delivery);
|
|
893
|
+
const redemptions = moneyNumber(applied[index] ?? moneyZero());
|
|
894
|
+
const published = money(subtotal).minus(discount).plus(tax).plus(fees).plus(tip).plus(delivery).minus(redemptions);
|
|
895
|
+
const b1 = {
|
|
896
|
+
currency,
|
|
897
|
+
subtotal,
|
|
898
|
+
discount_total: discount,
|
|
899
|
+
tax_total: tax
|
|
900
|
+
};
|
|
901
|
+
const b2 = {
|
|
902
|
+
fees,
|
|
903
|
+
tip,
|
|
904
|
+
delivery,
|
|
905
|
+
redemptions_applied: redemptions
|
|
906
|
+
};
|
|
907
|
+
const amount = {
|
|
908
|
+
...b1,
|
|
909
|
+
...b2,
|
|
910
|
+
total: moneyNumber(published)
|
|
911
|
+
};
|
|
912
|
+
const open = tender.allocation === null;
|
|
913
|
+
const lines = open ? [] : tender.allocation?.lines ?? [];
|
|
914
|
+
const feesLines = open ? [] : tender.allocation?.fees ?? [];
|
|
915
|
+
let allocation = null;
|
|
916
|
+
if (real.length > 1 || !open) {
|
|
917
|
+
const gross = money(amount.total).plus(amount.redemptions_applied);
|
|
918
|
+
allocation = {
|
|
919
|
+
lines,
|
|
920
|
+
fees: feesLines,
|
|
921
|
+
amounts: balanceAmounts(build.amounts, gross)
|
|
922
|
+
};
|
|
923
|
+
}
|
|
924
|
+
return {
|
|
925
|
+
...base(tender),
|
|
926
|
+
allocation,
|
|
927
|
+
amount
|
|
928
|
+
};
|
|
929
|
+
}), ...redemptionTenders].toSorted((a, b) => {
|
|
930
|
+
return a.sequence - b.sequence;
|
|
931
|
+
});
|
|
932
|
+
const primaryId = real[0]?.id ?? null;
|
|
933
|
+
const used = tenders.filter((tender) => tender.id !== primaryId).reduce((sum, tender) => {
|
|
934
|
+
return sum.plus(tender.amount.total);
|
|
935
|
+
}, moneyZero());
|
|
936
|
+
const primaryTotal = orderTotal.minus(used);
|
|
823
937
|
return {
|
|
824
938
|
ok: true,
|
|
825
|
-
tenders:
|
|
826
|
-
|
|
827
|
-
const
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
subtotal: fromMinor(build.products.subtotal)
|
|
831
|
-
};
|
|
832
|
-
const b2 = { discount_total: fromMinor(build.products.discount) };
|
|
833
|
-
const b3 = { tax_total: fromMinor(build.products.tax) };
|
|
834
|
-
const b4 = {
|
|
835
|
-
fees: fromMinor(build.fees),
|
|
836
|
-
tip: fromMinor(build.tip)
|
|
837
|
-
};
|
|
838
|
-
const b5 = { delivery: fromMinor(build.delivery) };
|
|
839
|
-
const b6 = { redemptions_applied: fromMinor(applied[index] ?? 0) };
|
|
939
|
+
tenders: tenders.map((tender) => {
|
|
940
|
+
if (tender.id !== primaryId) return tender;
|
|
941
|
+
const drift = primaryTotal.minus(tender.amount.total);
|
|
942
|
+
if (drift.eq(0)) return tender;
|
|
943
|
+
const subtotal = money(tender.amount.subtotal).plus(drift);
|
|
840
944
|
const amount = {
|
|
841
|
-
...
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
...b4,
|
|
845
|
-
...b5,
|
|
846
|
-
...b6,
|
|
847
|
-
total: fromMinor(total)
|
|
945
|
+
...tender.amount,
|
|
946
|
+
subtotal: moneyNumber(subtotal),
|
|
947
|
+
total: moneyNumber(primaryTotal)
|
|
848
948
|
};
|
|
849
|
-
const
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
if (real.length > 1 || !open) allocation = {
|
|
854
|
-
lines,
|
|
855
|
-
fees,
|
|
856
|
-
amounts: build.amounts
|
|
949
|
+
const allocation = tender.allocation;
|
|
950
|
+
if (!allocation) return {
|
|
951
|
+
...tender,
|
|
952
|
+
amount
|
|
857
953
|
};
|
|
954
|
+
const gross = money(amount.total).plus(amount.redemptions_applied);
|
|
955
|
+
const amounts = balanceAmounts(allocation.amounts, gross);
|
|
858
956
|
return {
|
|
859
|
-
...
|
|
860
|
-
|
|
861
|
-
|
|
957
|
+
...tender,
|
|
958
|
+
amount,
|
|
959
|
+
allocation: {
|
|
960
|
+
...allocation,
|
|
961
|
+
amounts
|
|
962
|
+
}
|
|
862
963
|
};
|
|
863
|
-
}), ...redemptionTenders].toSorted((a, b) => {
|
|
864
|
-
return a.sequence - b.sequence;
|
|
865
964
|
}),
|
|
866
|
-
primary_tender_id:
|
|
965
|
+
primary_tender_id: primaryId
|
|
867
966
|
};
|
|
868
967
|
};
|
|
869
968
|
/**
|
|
@@ -1171,10 +1270,11 @@ const isBatchCloseDue = (input) => {
|
|
|
1171
1270
|
const batchCloseDay = (now) => localDay(now);
|
|
1172
1271
|
//#endregion
|
|
1173
1272
|
//#region src/redemptions.ts
|
|
1174
|
-
const
|
|
1175
|
-
|
|
1273
|
+
const add = (left, right) => {
|
|
1274
|
+
return moneyNumber(money(left).plus(right));
|
|
1275
|
+
};
|
|
1176
1276
|
/**
|
|
1177
|
-
*
|
|
1277
|
+
* Reward (voucher, balance…) paid with a REDEMPTION tender, not a discount (D16):
|
|
1178
1278
|
* it never discounts lines nor has a `redemption_steps` entry.
|
|
1179
1279
|
*/
|
|
1180
1280
|
const isPaymentRedeem = (redeem) => {
|
|
@@ -1198,40 +1298,30 @@ const orderPaymentRedeems = (order) => {
|
|
|
1198
1298
|
* intent is only REDEMPTION tenders and the order advances on its own.
|
|
1199
1299
|
*/
|
|
1200
1300
|
const paymentRedeemsCoverTotal = (redeem, totals) => {
|
|
1201
|
-
const decimals = currencyDecimals(totals.currency);
|
|
1202
1301
|
const paid = redeem.reduce((sum, item) => {
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1302
|
+
if (!isPaymentRedeem(item)) return sum;
|
|
1303
|
+
return sum.plus(item.amount ?? 0);
|
|
1304
|
+
}, moneyZero());
|
|
1305
|
+
if (paid.lte(0)) return false;
|
|
1306
|
+
return paid.eq(money(totals.total));
|
|
1207
1307
|
};
|
|
1208
1308
|
const rewardOf = (redeem) => {
|
|
1209
|
-
const r1 = {
|
|
1210
|
-
reward_id: redeem.reward_id,
|
|
1211
|
-
quantity: redeem.quantity ?? 1
|
|
1212
|
-
};
|
|
1213
|
-
const r2 = {
|
|
1214
|
-
points: redeem.points ?? 0,
|
|
1215
|
-
points_value: redeem.amount ?? 0
|
|
1216
|
-
};
|
|
1217
1309
|
return {
|
|
1218
|
-
|
|
1219
|
-
|
|
1310
|
+
reward_id: redeem.reward_id,
|
|
1311
|
+
quantity: redeem.quantity ?? 1,
|
|
1312
|
+
amount: redeem.amount ?? 0
|
|
1220
1313
|
};
|
|
1221
1314
|
};
|
|
1222
|
-
const detailsOf = (rewards,
|
|
1315
|
+
const detailsOf = (rewards, loyalty_transaction_id) => {
|
|
1223
1316
|
const d1 = {
|
|
1224
1317
|
method: REDEMPTION_METHOD,
|
|
1225
1318
|
rewards,
|
|
1226
|
-
|
|
1227
|
-
points_value: rewards.reduce((sum, item) => add(sum, item.points_value), 0)
|
|
1319
|
+
amount: rewards.reduce((sum, item) => add(sum, item.amount), 0)
|
|
1228
1320
|
};
|
|
1229
|
-
const d2 =
|
|
1230
|
-
const d3 = loyalty_transaction_id ? { loyalty_transaction_id } : {};
|
|
1321
|
+
const d2 = loyalty_transaction_id ? { loyalty_transaction_id } : {};
|
|
1231
1322
|
return {
|
|
1232
1323
|
...d1,
|
|
1233
|
-
...d2
|
|
1234
|
-
...d3
|
|
1324
|
+
...d2
|
|
1235
1325
|
};
|
|
1236
1326
|
};
|
|
1237
1327
|
/**
|
|
@@ -1248,11 +1338,10 @@ const groupPaymentRedeems = (redeem) => {
|
|
|
1248
1338
|
groups.set(payment_id, list);
|
|
1249
1339
|
}
|
|
1250
1340
|
return [...groups].map(([payment_id, items]) => {
|
|
1251
|
-
const
|
|
1252
|
-
const details = detailsOf(items.map(rewardOf), program);
|
|
1341
|
+
const details = detailsOf(items.map(rewardOf));
|
|
1253
1342
|
return {
|
|
1254
1343
|
payment_id,
|
|
1255
|
-
amount: details.
|
|
1344
|
+
amount: details.amount,
|
|
1256
1345
|
details
|
|
1257
1346
|
};
|
|
1258
1347
|
});
|
|
@@ -1260,27 +1349,41 @@ const groupPaymentRedeems = (redeem) => {
|
|
|
1260
1349
|
const isRecord = (value) => {
|
|
1261
1350
|
return typeof value === "object" && value !== null;
|
|
1262
1351
|
};
|
|
1352
|
+
const numberOr = (value, fallback) => {
|
|
1353
|
+
return typeof value === "number" && Number.isFinite(value) ? value : fallback;
|
|
1354
|
+
};
|
|
1355
|
+
/** Money of a persisted reward: `amount`, or `points_value` before it. */
|
|
1356
|
+
const rewardAmountOf = (reward) => {
|
|
1357
|
+
return numberOr(reward.amount, numberOr(reward.points_value, 0));
|
|
1358
|
+
};
|
|
1359
|
+
const persistedRewardOf = (value) => {
|
|
1360
|
+
if (!isRecord(value) || typeof value.reward_id !== "string") return null;
|
|
1361
|
+
const quantity = numberOr(value.quantity, 1);
|
|
1362
|
+
return {
|
|
1363
|
+
reward_id: value.reward_id,
|
|
1364
|
+
quantity,
|
|
1365
|
+
amount: rewardAmountOf(value)
|
|
1366
|
+
};
|
|
1367
|
+
};
|
|
1263
1368
|
/**
|
|
1264
|
-
* REDEMPTION `details` in the
|
|
1265
|
-
*
|
|
1369
|
+
* REDEMPTION `details` in the current shape, or null for other methods.
|
|
1370
|
+
* Details persisted earlier (one `reward_id`, or money as `points_value`)
|
|
1371
|
+
* read as the same shape; their old keys are dropped.
|
|
1266
1372
|
*/
|
|
1267
1373
|
const redemptionDetailsOf = (details) => {
|
|
1268
1374
|
if (!isRecord(details) || details.method !== "REDEMPTION") return null;
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1375
|
+
const id = details.loyalty_transaction_id;
|
|
1376
|
+
const reference = typeof id === "string" ? id : void 0;
|
|
1377
|
+
if (Array.isArray(details.rewards)) {
|
|
1378
|
+
const rewards = details.rewards.map(persistedRewardOf);
|
|
1379
|
+
if (rewards.some((reward) => reward === null)) return null;
|
|
1380
|
+
return detailsOf(rewards, reference);
|
|
1381
|
+
}
|
|
1382
|
+
const reward = persistedRewardOf({
|
|
1383
|
+
...details,
|
|
1274
1384
|
quantity: 1
|
|
1275
|
-
};
|
|
1276
|
-
|
|
1277
|
-
const reward = {
|
|
1278
|
-
...r1,
|
|
1279
|
-
...r2,
|
|
1280
|
-
points_value: legacy.points_value ?? 0
|
|
1281
|
-
};
|
|
1282
|
-
const id = legacy.loyalty_transaction_id;
|
|
1283
|
-
return detailsOf([reward], legacy.program, id);
|
|
1385
|
+
});
|
|
1386
|
+
return reward ? detailsOf([reward], reference) : null;
|
|
1284
1387
|
};
|
|
1285
1388
|
/** Rewards of a REDEMPTION tender; empty for other methods. */
|
|
1286
1389
|
const redemptionRewardsOf = (details) => {
|
|
@@ -1322,24 +1425,24 @@ const createAttemptReference = (now = Date.now()) => {
|
|
|
1322
1425
|
* its children). `share` prorates it when the intent covers part of the order.
|
|
1323
1426
|
*/
|
|
1324
1427
|
const taxBreakdown = (lines, options = {}) => {
|
|
1325
|
-
const share = options.share ?? 1;
|
|
1326
|
-
const decimals = options.decimals ?? 2;
|
|
1428
|
+
const share = money(options.share ?? 1);
|
|
1327
1429
|
const byRate = /* @__PURE__ */ new Map();
|
|
1328
1430
|
for (const line of lines) {
|
|
1329
1431
|
const rate = line.tax_rate ?? 0;
|
|
1330
1432
|
const current = byRate.get(rate) ?? {
|
|
1331
|
-
base:
|
|
1332
|
-
tax:
|
|
1433
|
+
base: moneyZero(),
|
|
1434
|
+
tax: moneyZero()
|
|
1333
1435
|
};
|
|
1334
|
-
|
|
1335
|
-
current.
|
|
1436
|
+
const base = money(line.subtotal).minus(line.discount_total);
|
|
1437
|
+
current.base = current.base.plus(base);
|
|
1438
|
+
current.tax = current.tax.plus(line.tax_total);
|
|
1336
1439
|
byRate.set(rate, current);
|
|
1337
1440
|
}
|
|
1338
1441
|
return [...byRate.entries()].toSorted(([a], [b]) => a - b).map(([rate, sums]) => {
|
|
1339
1442
|
return {
|
|
1340
1443
|
rate,
|
|
1341
|
-
base:
|
|
1342
|
-
tax:
|
|
1444
|
+
base: moneyNumber(sums.base.times(share)),
|
|
1445
|
+
tax: moneyNumber(sums.tax.times(share))
|
|
1343
1446
|
};
|
|
1344
1447
|
});
|
|
1345
1448
|
};
|
|
@@ -1384,4 +1487,4 @@ const memoryPaymentStorage = () => {
|
|
|
1384
1487
|
};
|
|
1385
1488
|
};
|
|
1386
1489
|
//#endregion
|
|
1387
|
-
export { ACTIVE_INTENT_STATUSES, CURRENCY_DECIMALS, DEFAULT_PAYMENT_POLICY, DELIVERY_FEE_BASED_ON, DIGITAL_COLLECT_AT, NEXT_STATUSES, PARTIAL_APPROVAL_ERROR, PAYMENT_ERROR_CODES, PAYMENT_ERROR_DEFAULTS, PaymentError, REDEMPTION_METHOD, TERMINAL_TENDER_STATUSES, TIP_FEE_BASED_ON, activeIntent, allocateTenders, amountTolerance, batchCloseDay, countTenderItems, createAttemptReference, createPaymentError, currencyDecimals, currentIntent, defaultReversalFor, definePaymentErrors, derivePaymentStatus, discountRedeems, formatPaymentError, groupPaymentRedeems, isBatchCloseDue, isDeliveryFee, isIntentCovered, isMixedTenders, isPaymentError, isPaymentRedeem, isRealPaymentMethod, isRedemptionReserved, isScopePaid, isTenderSettled, isTerminalLocked, isTipFee, liveRealTenders, liveTenders, memoryPaymentStorage, mergePaymentTender, missingDeviceFields, nextIntentStatus, orderPaymentRedeems, paymentRedeemsCoverTotal, primaryTender, primaryTenderOf, prorateMinor, realTenders, redemptionDetailsOf, redemptionRewardsOf, resolvePaymentPolicy, resolveProviderEnvironment, roundTo, scopedPaymentStorage, taxBreakdown, tendersCoverage, tendersInScope, toChargeAmount, toPaymentAmount, toPaymentError, validateTendersAgainstRules, withTerminalLock };
|
|
1490
|
+
export { ACTIVE_INTENT_STATUSES, CURRENCY_DECIMALS, DEFAULT_PAYMENT_POLICY, DELIVERY_FEE_BASED_ON, DIGITAL_COLLECT_AT, NEXT_STATUSES, PARTIAL_APPROVAL_ERROR, PAYMENT_ERROR_CODES, PAYMENT_ERROR_DEFAULTS, PaymentError, REDEMPTION_METHOD, TERMINAL_TENDER_STATUSES, TIP_FEE_BASED_ON, activeIntent, allocateTenders, amountTolerance, batchCloseDay, countTenderItems, createAttemptReference, createPaymentError, currencyDecimals, currentIntent, defaultReversalFor, definePaymentErrors, derivePaymentStatus, discountRedeems, formatPaymentError, groupPaymentRedeems, isBatchCloseDue, isDeliveryFee, isIntentCovered, isMixedTenders, isPaymentError, isPaymentRedeem, isRealPaymentMethod, isRedemptionReserved, isScopePaid, isTenderSettled, isTerminalLocked, isTipFee, liveRealTenders, liveTenders, memoryPaymentStorage, mergePaymentTender, missingDeviceFields, money, moneyMax, moneyMin, moneyNumber, moneyNumbers, moneySum, moneyZero, nextIntentStatus, orderPaymentRedeems, paymentRedeemsCoverTotal, primaryTender, primaryTenderOf, prorateMinor, realTenders, redemptionDetailsOf, redemptionRewardsOf, resolvePaymentPolicy, resolveProviderEnvironment, roundTo, scopedPaymentStorage, splitMoney, taxBreakdown, tendersCoverage, tendersInScope, toChargeAmount, toPaymentAmount, toPaymentError, validateTendersAgainstRules, withTerminalLock };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fire-spark/payments",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.1",
|
|
4
4
|
"description": "Fire Spark payments contract: adapters, errors, amounts and policy",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"sideEffects": false,
|
|
@@ -17,9 +17,11 @@
|
|
|
17
17
|
}
|
|
18
18
|
},
|
|
19
19
|
"dependencies": {
|
|
20
|
-
"
|
|
20
|
+
"big.js": "6.2.1",
|
|
21
|
+
"@fire-spark/types": "^0.4.1"
|
|
21
22
|
},
|
|
22
23
|
"devDependencies": {
|
|
24
|
+
"@types/big.js": "6.2.2",
|
|
23
25
|
"tsdown": "^0.22.14",
|
|
24
26
|
"typescript": "^7.0.2",
|
|
25
27
|
"vitest": "^2.1.8"
|