@fire-spark/payments 0.4.1 → 0.5.0
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 +26 -29
- package/dist/index.js +103 -156
- package/package.json +2 -2
package/dist/index.d.ts
CHANGED
|
@@ -57,7 +57,9 @@ declare const prorateMinor: (amount: number, quantity: number, parts: readonly {
|
|
|
57
57
|
* split exactly (last part is the remainder), fees to their tender, tip /
|
|
58
58
|
* delivery and any order-level difference to the primary, then REDEMPTION
|
|
59
59
|
* deducted from the primary first and by `sequence`, never below a tender's
|
|
60
|
-
* tip + delivery (D18).
|
|
60
|
+
* tip + delivery (D18). A tender publishes only what it pays
|
|
61
|
+
* (`amount.total`); `allocation.amounts` is the money of the lines it
|
|
62
|
+
* covers, before REDEMPTION. Amounts are not rounded.
|
|
61
63
|
*/
|
|
62
64
|
declare const allocateTenders: (input: AllocateTendersInput) => AllocateTendersResult;
|
|
63
65
|
/**
|
|
@@ -97,8 +99,8 @@ declare const validateTendersAgainstRules: (input: {
|
|
|
97
99
|
//#region src/money.d.ts
|
|
98
100
|
/** One monetary amount. Arithmetic stays on Big until a caller serializes. */
|
|
99
101
|
type Money = Big;
|
|
100
|
-
declare const money: (value: number | string | Money) =>
|
|
101
|
-
declare const moneyZero: () =>
|
|
102
|
+
declare const money: (value: number | string | Money) => Money;
|
|
103
|
+
declare const moneyZero: () => Money;
|
|
102
104
|
declare const moneySum: (values: readonly Money[]) => Big;
|
|
103
105
|
declare const moneyMax: (left: Money, right: Money) => Big;
|
|
104
106
|
declare const moneyMin: (left: Money, right: Money) => Big;
|
|
@@ -455,8 +457,6 @@ declare const toChargeAmount: (input: {
|
|
|
455
457
|
chargeCurrency?: string | null;
|
|
456
458
|
rate?: number | null;
|
|
457
459
|
}) => ChargeConversion;
|
|
458
|
-
/** Tolerance used when comparing amounts in a currency. */
|
|
459
|
-
declare const amountTolerance: (currency: string) => number;
|
|
460
460
|
//#endregion
|
|
461
461
|
//#region src/device.d.ts
|
|
462
462
|
/** Required, visible device fields that are still empty on this device. */
|
|
@@ -495,16 +495,17 @@ type RedemptionDetails = Extract<TenderDetails, {
|
|
|
495
495
|
/** One reward inside a grouped REDEMPTION tender (entry totals). */
|
|
496
496
|
type RedemptionReward = RedemptionDetails["rewards"][number];
|
|
497
497
|
/**
|
|
498
|
-
*
|
|
499
|
-
* the entry (every unit); `quantity` is informational.
|
|
498
|
+
* Redeem as stored in `orders.redeem` or a root line's `redeem`. `amount`
|
|
499
|
+
* is the total of the entry (every unit); `quantity` is informational.
|
|
500
500
|
*/
|
|
501
|
-
type
|
|
501
|
+
type RedeemLike = {
|
|
502
502
|
reward_id: string;
|
|
503
503
|
quantity?: number;
|
|
504
504
|
payment_id?: string | null;
|
|
505
505
|
amount?: number;
|
|
506
|
+
before_taxes?: boolean;
|
|
506
507
|
};
|
|
507
|
-
/**
|
|
508
|
+
/** Redeems of one `payment_id`, ready to seed its tender. */
|
|
508
509
|
type RedemptionGroup = {
|
|
509
510
|
payment_id: string;
|
|
510
511
|
/** Tender value: Σ `amount` of the grouped redeems. */
|
|
@@ -512,18 +513,14 @@ type RedemptionGroup = {
|
|
|
512
513
|
details: RedemptionDetails;
|
|
513
514
|
};
|
|
514
515
|
/**
|
|
515
|
-
*
|
|
516
|
-
*
|
|
516
|
+
* Every redeem has a `payment_id` whose REDEMPTION tender covers it.
|
|
517
|
+
* Legacy rows stored without one are ignored.
|
|
517
518
|
*/
|
|
518
|
-
declare const
|
|
519
|
+
declare const hasRedeemPayment: (redeem: {
|
|
519
520
|
payment_id?: string | null;
|
|
520
521
|
}) => boolean;
|
|
521
|
-
/**
|
|
522
|
-
|
|
523
|
-
payment_id?: string | null;
|
|
524
|
-
}>(redeem: readonly T[]) => T[];
|
|
525
|
-
/** Order fields payment redeems live in: the order and its root lines. */
|
|
526
|
-
type PaymentRedeemSource<T extends PaymentRedeemLike> = {
|
|
522
|
+
/** Order fields redeems live in: the order and its root lines. */
|
|
523
|
+
type RedeemSource<T extends RedeemLike> = {
|
|
527
524
|
redeem?: readonly T[] | null;
|
|
528
525
|
lines?: readonly {
|
|
529
526
|
parent_uid?: string | null;
|
|
@@ -531,24 +528,24 @@ type PaymentRedeemSource<T extends PaymentRedeemLike> = {
|
|
|
531
528
|
}[];
|
|
532
529
|
};
|
|
533
530
|
/**
|
|
534
|
-
* Every
|
|
535
|
-
*
|
|
536
|
-
*
|
|
531
|
+
* Every redeem of the order: order-level ones first, then the ones kept
|
|
532
|
+
* on a root line (a voucher stored with the product it pays for). Each is
|
|
533
|
+
* covered by a REDEMPTION tender; child-line redeems never apply.
|
|
537
534
|
*/
|
|
538
|
-
declare const
|
|
535
|
+
declare const orderRedeems: <T extends RedeemLike>(order: RedeemSource<T>) => T[];
|
|
539
536
|
/**
|
|
540
|
-
*
|
|
541
|
-
*
|
|
537
|
+
* Redeems pay the whole order, so no other tender is needed: the intent
|
|
538
|
+
* is only REDEMPTION tenders and the order advances on its own.
|
|
542
539
|
*/
|
|
543
|
-
declare const
|
|
540
|
+
declare const redeemsCoverTotal: (redeem: readonly RedeemLike[], totals: {
|
|
544
541
|
total: number;
|
|
545
542
|
currency: string;
|
|
546
543
|
}) => boolean;
|
|
547
544
|
/**
|
|
548
|
-
* Groups
|
|
549
|
-
*
|
|
545
|
+
* Groups redeems into one entry per `payment_id`, in first-appearance
|
|
546
|
+
* order. Legacy entries without `payment_id` are skipped.
|
|
550
547
|
*/
|
|
551
|
-
declare const
|
|
548
|
+
declare const groupRedeems: (redeem: readonly RedeemLike[]) => RedemptionGroup[];
|
|
552
549
|
/**
|
|
553
550
|
* REDEMPTION `details` in the current shape, or null for other methods.
|
|
554
551
|
* Details persisted earlier (one `reward_id`, or money as `points_value`)
|
|
@@ -723,4 +720,4 @@ declare const scopedPaymentStorage: (storage: PaymentStorage, paymentId: string)
|
|
|
723
720
|
/** For tests and web previews; apps provide AsyncStorage or localStorage. */
|
|
724
721
|
declare const memoryPaymentStorage: () => PaymentStorage;
|
|
725
722
|
//#endregion
|
|
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,
|
|
723
|
+
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, type PaymentResult, type PaymentReversal, type PaymentServer, type PaymentStorage, PaymentTenderUpdate, type PaymentUI, REDEMPTION_METHOD, RawPaymentError, RedeemLike, RedeemSource, 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, batchCloseDay, countTenderItems, createAttemptReference, createPaymentError, currencyDecimals, currentIntent, defaultReversalFor, definePaymentErrors, derivePaymentStatus, formatPaymentError, groupRedeems, hasRedeemPayment, isBatchCloseDue, isDeliveryFee, isIntentCovered, isMixedTenders, isPaymentError, isRealPaymentMethod, isRedemptionReserved, isScopePaid, isTenderSettled, isTerminalLocked, isTipFee, liveRealTenders, liveTenders, memoryPaymentStorage, mergePaymentTender, missingDeviceFields, money, moneyMax, moneyMin, moneyNumber, moneyNumbers, moneySum, moneyZero, nextIntentStatus, orderRedeems, primaryTender, primaryTenderOf, prorateMinor, realTenders, redeemsCoverTotal, redemptionDetailsOf, redemptionRewardsOf, resolvePaymentPolicy, resolveProviderEnvironment, roundTo, scopedPaymentStorage, splitMoney, taxBreakdown, tendersCoverage, tendersInScope, toChargeAmount, toPaymentAmount, toPaymentError, validateTendersAgainstRules, withTerminalLock };
|
package/dist/index.js
CHANGED
|
@@ -42,65 +42,6 @@ const moneyNumbers = (whole, parts) => {
|
|
|
42
42
|
return [...head, whole.minus(used).toNumber()];
|
|
43
43
|
};
|
|
44
44
|
//#endregion
|
|
45
|
-
//#region src/amount.ts
|
|
46
|
-
/** ISO 4217 minor units; anything not listed uses 2. */
|
|
47
|
-
const CURRENCY_DECIMALS = {
|
|
48
|
-
CLP: 0,
|
|
49
|
-
PYG: 0,
|
|
50
|
-
JPY: 0,
|
|
51
|
-
COP: 2,
|
|
52
|
-
USD: 2,
|
|
53
|
-
BRL: 2,
|
|
54
|
-
PEN: 2,
|
|
55
|
-
ARS: 2,
|
|
56
|
-
VES: 2,
|
|
57
|
-
MXN: 2,
|
|
58
|
-
EUR: 2
|
|
59
|
-
};
|
|
60
|
-
const currencyDecimals = (currency) => {
|
|
61
|
-
return CURRENCY_DECIMALS[currency.toUpperCase()] ?? 2;
|
|
62
|
-
};
|
|
63
|
-
/** Full precision. `decimals` is ignored so amounts are never truncated. */
|
|
64
|
-
const roundTo = (value, _decimals) => {
|
|
65
|
-
return moneyNumber(money(value));
|
|
66
|
-
};
|
|
67
|
-
const toPaymentAmount = (value, currency) => {
|
|
68
|
-
const code = currency.toUpperCase();
|
|
69
|
-
const decimals = currencyDecimals(code);
|
|
70
|
-
const exact = money(value);
|
|
71
|
-
const scale = money(10).pow(decimals);
|
|
72
|
-
const minor = moneyNumber(exact.times(scale));
|
|
73
|
-
return {
|
|
74
|
-
value: moneyNumber(exact),
|
|
75
|
-
minor,
|
|
76
|
-
currency: code,
|
|
77
|
-
decimals
|
|
78
|
-
};
|
|
79
|
-
};
|
|
80
|
-
/** Converts an order-currency value to the charge currency at a fixed rate. */
|
|
81
|
-
const toChargeAmount = (input) => {
|
|
82
|
-
const orderCurrency = input.orderCurrency.toUpperCase();
|
|
83
|
-
const chargeCurrency = (input.chargeCurrency ?? orderCurrency).toUpperCase();
|
|
84
|
-
const same = chargeCurrency === orderCurrency;
|
|
85
|
-
if (!same && !(input.rate && input.rate > 0)) {
|
|
86
|
-
const message = `Missing exchange rate ${orderCurrency}->${chargeCurrency}`;
|
|
87
|
-
throw new Error(message);
|
|
88
|
-
}
|
|
89
|
-
const rate = same ? 1 : input.rate;
|
|
90
|
-
const charged = money(input.value).times(rate);
|
|
91
|
-
const amount = toPaymentAmount(moneyNumber(charged), chargeCurrency);
|
|
92
|
-
return {
|
|
93
|
-
amount,
|
|
94
|
-
charged_amount: amount.value,
|
|
95
|
-
charged_currency: chargeCurrency,
|
|
96
|
-
exchange_rate: rate
|
|
97
|
-
};
|
|
98
|
-
};
|
|
99
|
-
/** Tolerance used when comparing amounts in a currency. */
|
|
100
|
-
const amountTolerance = (currency) => {
|
|
101
|
-
return 10 ** -currencyDecimals(currency) / 2;
|
|
102
|
-
};
|
|
103
|
-
//#endregion
|
|
104
45
|
//#region src/tenders.ts
|
|
105
46
|
const REDEMPTION_METHOD = "REDEMPTION";
|
|
106
47
|
/** Methods that charge money (everything but `REDEMPTION`, D17). */
|
|
@@ -199,17 +140,20 @@ const coveringTenders = (tenders) => {
|
|
|
199
140
|
return tender.status !== "FAILED" && tender.status !== "CANCELLED";
|
|
200
141
|
});
|
|
201
142
|
};
|
|
143
|
+
const coverageOf = (tenders) => {
|
|
144
|
+
return coveringTenders(tenders).reduce((sum, tender) => {
|
|
145
|
+
return sum.plus(tender.amount.total);
|
|
146
|
+
}, moneyZero());
|
|
147
|
+
};
|
|
202
148
|
/** Σ `amount.total` of live, not failed/cancelled tenders. */
|
|
203
149
|
const tendersCoverage = (tenders) => {
|
|
204
|
-
return
|
|
205
|
-
return sum + tender.amount.total;
|
|
206
|
-
}, 0);
|
|
150
|
+
return moneyNumber(coverageOf(tenders));
|
|
207
151
|
};
|
|
208
152
|
/** Live tenders that are not failed/cancelled cover the order total. */
|
|
209
153
|
const isIntentCovered = (intent, orderTotal) => {
|
|
210
154
|
if (!intent) return false;
|
|
211
155
|
const total = orderTotal ?? intent.totals.total;
|
|
212
|
-
return
|
|
156
|
+
return coverageOf(liveTenders(intent)).gte(money(total));
|
|
213
157
|
};
|
|
214
158
|
/**
|
|
215
159
|
* Order `payment_status` from the live tenders of the current intent
|
|
@@ -266,8 +210,7 @@ const RESOLVED_STATUSES = [
|
|
|
266
210
|
const isPartialApproval = (tender, update) => {
|
|
267
211
|
if (update.status !== "PAID") return false;
|
|
268
212
|
if (update.approved_amount === void 0) return false;
|
|
269
|
-
|
|
270
|
-
return update.approved_amount + tolerance < tender.amount.total;
|
|
213
|
+
return money(update.approved_amount).lt(tender.amount.total);
|
|
271
214
|
};
|
|
272
215
|
const withAttempt = (tender, reference) => {
|
|
273
216
|
if (tender.attempt_reference === reference) return tender;
|
|
@@ -635,32 +578,14 @@ const balanceAmounts = (amounts, gross) => {
|
|
|
635
578
|
const used = head.reduce((sum, row) => sum.plus(row.total), moneyZero());
|
|
636
579
|
const lastTotal = gross.minus(used);
|
|
637
580
|
const last = amounts[amounts.length - 1];
|
|
638
|
-
|
|
639
|
-
const subtotal = money(last.subtotal).plus(drift);
|
|
640
|
-
const total = moneyNumber(lastTotal);
|
|
641
|
-
const next = {
|
|
581
|
+
return [...head, {
|
|
642
582
|
...last,
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
};
|
|
646
|
-
return [...head, next];
|
|
583
|
+
total: moneyNumber(lastTotal)
|
|
584
|
+
}];
|
|
647
585
|
};
|
|
648
586
|
const emptyAmount = (currency, total) => {
|
|
649
|
-
const b1 = {
|
|
650
|
-
currency,
|
|
651
|
-
subtotal: total,
|
|
652
|
-
discount_total: 0,
|
|
653
|
-
tax_total: 0
|
|
654
|
-
};
|
|
655
|
-
const b2 = {
|
|
656
|
-
fees: 0,
|
|
657
|
-
tip: 0,
|
|
658
|
-
delivery: 0,
|
|
659
|
-
redemptions_applied: 0
|
|
660
|
-
};
|
|
661
587
|
return {
|
|
662
|
-
|
|
663
|
-
...b2,
|
|
588
|
+
currency,
|
|
664
589
|
total
|
|
665
590
|
};
|
|
666
591
|
};
|
|
@@ -669,7 +594,9 @@ const emptyAmount = (currency, total) => {
|
|
|
669
594
|
* split exactly (last part is the remainder), fees to their tender, tip /
|
|
670
595
|
* delivery and any order-level difference to the primary, then REDEMPTION
|
|
671
596
|
* deducted from the primary first and by `sequence`, never below a tender's
|
|
672
|
-
* tip + delivery (D18).
|
|
597
|
+
* tip + delivery (D18). A tender publishes only what it pays
|
|
598
|
+
* (`amount.total`); `allocation.amounts` is the money of the lines it
|
|
599
|
+
* covers, before REDEMPTION. Amounts are not rounded.
|
|
673
600
|
*/
|
|
674
601
|
const allocateTenders = (input) => {
|
|
675
602
|
const currency = input.totals.currency;
|
|
@@ -754,19 +681,10 @@ const allocateTenders = (input) => {
|
|
|
754
681
|
};
|
|
755
682
|
});
|
|
756
683
|
const toAmount = (uid, value, units) => {
|
|
757
|
-
const subtotal = moneyNumber(value.subtotal);
|
|
758
|
-
const discount = moneyNumber(value.discount);
|
|
759
|
-
const total = moneyNumber(value.total);
|
|
760
|
-
const b1 = {
|
|
761
|
-
tax: moneyNumber(money(total).minus(subtotal).plus(discount)),
|
|
762
|
-
total
|
|
763
|
-
};
|
|
764
684
|
return {
|
|
765
685
|
line_uid: uid,
|
|
766
686
|
...units === void 0 ? {} : { units },
|
|
767
|
-
|
|
768
|
-
discount,
|
|
769
|
-
...b1
|
|
687
|
+
total: moneyNumber(value.total)
|
|
770
688
|
};
|
|
771
689
|
};
|
|
772
690
|
for (const root of roots) {
|
|
@@ -882,45 +800,26 @@ const allocateTenders = (input) => {
|
|
|
882
800
|
message: "Redemptions exceed what real tenders can absorb"
|
|
883
801
|
}
|
|
884
802
|
};
|
|
803
|
+
const appliedById = /* @__PURE__ */ new Map();
|
|
885
804
|
const tenders = [...real.map((tender, index) => {
|
|
886
805
|
const build = builds[index];
|
|
887
|
-
const
|
|
888
|
-
const
|
|
889
|
-
|
|
890
|
-
const
|
|
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
|
-
};
|
|
806
|
+
const gross = totals[index] ?? moneyZero();
|
|
807
|
+
const redemptions = applied[index] ?? moneyZero();
|
|
808
|
+
appliedById.set(tender.id, redemptions);
|
|
809
|
+
const published = gross.minus(redemptions);
|
|
907
810
|
const amount = {
|
|
908
|
-
|
|
909
|
-
...b2,
|
|
811
|
+
currency,
|
|
910
812
|
total: moneyNumber(published)
|
|
911
813
|
};
|
|
912
814
|
const open = tender.allocation === null;
|
|
913
815
|
const lines = open ? [] : tender.allocation?.lines ?? [];
|
|
914
816
|
const feesLines = open ? [] : tender.allocation?.fees ?? [];
|
|
915
817
|
let allocation = null;
|
|
916
|
-
if (real.length > 1 || !open) {
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
amounts: balanceAmounts(build.amounts, gross)
|
|
922
|
-
};
|
|
923
|
-
}
|
|
818
|
+
if (real.length > 1 || !open) allocation = {
|
|
819
|
+
lines,
|
|
820
|
+
fees: feesLines,
|
|
821
|
+
amounts: balanceAmounts(build.amounts, gross)
|
|
822
|
+
};
|
|
924
823
|
return {
|
|
925
824
|
...base(tender),
|
|
926
825
|
allocation,
|
|
@@ -938,12 +837,9 @@ const allocateTenders = (input) => {
|
|
|
938
837
|
ok: true,
|
|
939
838
|
tenders: tenders.map((tender) => {
|
|
940
839
|
if (tender.id !== primaryId) return tender;
|
|
941
|
-
|
|
942
|
-
if (drift.eq(0)) return tender;
|
|
943
|
-
const subtotal = money(tender.amount.subtotal).plus(drift);
|
|
840
|
+
if (primaryTotal.eq(tender.amount.total)) return tender;
|
|
944
841
|
const amount = {
|
|
945
842
|
...tender.amount,
|
|
946
|
-
subtotal: moneyNumber(subtotal),
|
|
947
843
|
total: moneyNumber(primaryTotal)
|
|
948
844
|
};
|
|
949
845
|
const allocation = tender.allocation;
|
|
@@ -951,7 +847,8 @@ const allocateTenders = (input) => {
|
|
|
951
847
|
...tender,
|
|
952
848
|
amount
|
|
953
849
|
};
|
|
954
|
-
const
|
|
850
|
+
const redemptions = appliedById.get(tender.id) ?? moneyZero();
|
|
851
|
+
const gross = primaryTotal.plus(redemptions);
|
|
955
852
|
const amounts = balanceAmounts(allocation.amounts, gross);
|
|
956
853
|
return {
|
|
957
854
|
...tender,
|
|
@@ -1021,8 +918,7 @@ const validateTendersAgainstRules = (input) => {
|
|
|
1021
918
|
}
|
|
1022
919
|
};
|
|
1023
920
|
const maxAmount = caps?.max_amount ?? effect.max_amount_per_method;
|
|
1024
|
-
|
|
1025
|
-
if (maxAmount !== null && tender.amount.total > maxAmount + tolerance) return {
|
|
921
|
+
if (maxAmount !== null && money(tender.amount.total).gt(maxAmount)) return {
|
|
1026
922
|
ok: false,
|
|
1027
923
|
error: {
|
|
1028
924
|
code: "PAYMENT_AMOUNT_EXCEEDED",
|
|
@@ -1037,6 +933,61 @@ const validateTendersAgainstRules = (input) => {
|
|
|
1037
933
|
};
|
|
1038
934
|
};
|
|
1039
935
|
//#endregion
|
|
936
|
+
//#region src/amount.ts
|
|
937
|
+
/** ISO 4217 minor units; anything not listed uses 2. */
|
|
938
|
+
const CURRENCY_DECIMALS = {
|
|
939
|
+
CLP: 0,
|
|
940
|
+
PYG: 0,
|
|
941
|
+
JPY: 0,
|
|
942
|
+
COP: 2,
|
|
943
|
+
USD: 2,
|
|
944
|
+
BRL: 2,
|
|
945
|
+
PEN: 2,
|
|
946
|
+
ARS: 2,
|
|
947
|
+
VES: 2,
|
|
948
|
+
MXN: 2,
|
|
949
|
+
EUR: 2
|
|
950
|
+
};
|
|
951
|
+
const currencyDecimals = (currency) => {
|
|
952
|
+
return CURRENCY_DECIMALS[currency.toUpperCase()] ?? 2;
|
|
953
|
+
};
|
|
954
|
+
/** Full precision. `decimals` is ignored so amounts are never truncated. */
|
|
955
|
+
const roundTo = (value, _decimals) => {
|
|
956
|
+
return moneyNumber(money(value));
|
|
957
|
+
};
|
|
958
|
+
const toPaymentAmount = (value, currency) => {
|
|
959
|
+
const code = currency.toUpperCase();
|
|
960
|
+
const decimals = currencyDecimals(code);
|
|
961
|
+
const exact = money(value);
|
|
962
|
+
const scale = money(10).pow(decimals);
|
|
963
|
+
const minor = moneyNumber(exact.times(scale));
|
|
964
|
+
return {
|
|
965
|
+
value: moneyNumber(exact),
|
|
966
|
+
minor,
|
|
967
|
+
currency: code,
|
|
968
|
+
decimals
|
|
969
|
+
};
|
|
970
|
+
};
|
|
971
|
+
/** Converts an order-currency value to the charge currency at a fixed rate. */
|
|
972
|
+
const toChargeAmount = (input) => {
|
|
973
|
+
const orderCurrency = input.orderCurrency.toUpperCase();
|
|
974
|
+
const chargeCurrency = (input.chargeCurrency ?? orderCurrency).toUpperCase();
|
|
975
|
+
const same = chargeCurrency === orderCurrency;
|
|
976
|
+
if (!same && !(input.rate && input.rate > 0)) {
|
|
977
|
+
const message = `Missing exchange rate ${orderCurrency}->${chargeCurrency}`;
|
|
978
|
+
throw new Error(message);
|
|
979
|
+
}
|
|
980
|
+
const rate = same ? 1 : input.rate;
|
|
981
|
+
const charged = money(input.value).times(rate);
|
|
982
|
+
const amount = toPaymentAmount(moneyNumber(charged), chargeCurrency);
|
|
983
|
+
return {
|
|
984
|
+
amount,
|
|
985
|
+
charged_amount: amount.value,
|
|
986
|
+
charged_currency: chargeCurrency,
|
|
987
|
+
exchange_rate: rate
|
|
988
|
+
};
|
|
989
|
+
};
|
|
990
|
+
//#endregion
|
|
1040
991
|
//#region src/device.ts
|
|
1041
992
|
const isEmpty = (value) => {
|
|
1042
993
|
return value == null || typeof value === "string" && value.trim() === "";
|
|
@@ -1274,32 +1225,28 @@ const add = (left, right) => {
|
|
|
1274
1225
|
return moneyNumber(money(left).plus(right));
|
|
1275
1226
|
};
|
|
1276
1227
|
/**
|
|
1277
|
-
*
|
|
1278
|
-
*
|
|
1228
|
+
* Every redeem has a `payment_id` whose REDEMPTION tender covers it.
|
|
1229
|
+
* Legacy rows stored without one are ignored.
|
|
1279
1230
|
*/
|
|
1280
|
-
const
|
|
1231
|
+
const hasRedeemPayment = (redeem) => {
|
|
1281
1232
|
return Boolean(redeem.payment_id);
|
|
1282
1233
|
};
|
|
1283
|
-
/** Discount redeems: the ones replayed into `redemption_steps`. */
|
|
1284
|
-
const discountRedeems = (redeem) => {
|
|
1285
|
-
return redeem.filter((item) => !isPaymentRedeem(item));
|
|
1286
|
-
};
|
|
1287
1234
|
/**
|
|
1288
|
-
* Every
|
|
1289
|
-
*
|
|
1290
|
-
*
|
|
1235
|
+
* Every redeem of the order: order-level ones first, then the ones kept
|
|
1236
|
+
* on a root line (a voucher stored with the product it pays for). Each is
|
|
1237
|
+
* covered by a REDEMPTION tender; child-line redeems never apply.
|
|
1291
1238
|
*/
|
|
1292
|
-
const
|
|
1239
|
+
const orderRedeems = (order) => {
|
|
1293
1240
|
const onLines = (order.lines ?? []).filter((line) => line.parent_uid == null).flatMap((line) => line.redeem ?? []);
|
|
1294
|
-
return [...order.redeem ?? [], ...onLines].filter(
|
|
1241
|
+
return [...order.redeem ?? [], ...onLines].filter(hasRedeemPayment);
|
|
1295
1242
|
};
|
|
1296
1243
|
/**
|
|
1297
|
-
*
|
|
1298
|
-
*
|
|
1244
|
+
* Redeems pay the whole order, so no other tender is needed: the intent
|
|
1245
|
+
* is only REDEMPTION tenders and the order advances on its own.
|
|
1299
1246
|
*/
|
|
1300
|
-
const
|
|
1247
|
+
const redeemsCoverTotal = (redeem, totals) => {
|
|
1301
1248
|
const paid = redeem.reduce((sum, item) => {
|
|
1302
|
-
if (!
|
|
1249
|
+
if (!hasRedeemPayment(item)) return sum;
|
|
1303
1250
|
return sum.plus(item.amount ?? 0);
|
|
1304
1251
|
}, moneyZero());
|
|
1305
1252
|
if (paid.lte(0)) return false;
|
|
@@ -1325,13 +1272,13 @@ const detailsOf = (rewards, loyalty_transaction_id) => {
|
|
|
1325
1272
|
};
|
|
1326
1273
|
};
|
|
1327
1274
|
/**
|
|
1328
|
-
* Groups
|
|
1329
|
-
*
|
|
1275
|
+
* Groups redeems into one entry per `payment_id`, in first-appearance
|
|
1276
|
+
* order. Legacy entries without `payment_id` are skipped.
|
|
1330
1277
|
*/
|
|
1331
|
-
const
|
|
1278
|
+
const groupRedeems = (redeem) => {
|
|
1332
1279
|
const groups = /* @__PURE__ */ new Map();
|
|
1333
1280
|
for (const item of redeem) {
|
|
1334
|
-
if (!
|
|
1281
|
+
if (!hasRedeemPayment(item)) continue;
|
|
1335
1282
|
const payment_id = item.payment_id;
|
|
1336
1283
|
const list = groups.get(payment_id) ?? [];
|
|
1337
1284
|
list.push(item);
|
|
@@ -1487,4 +1434,4 @@ const memoryPaymentStorage = () => {
|
|
|
1487
1434
|
};
|
|
1488
1435
|
};
|
|
1489
1436
|
//#endregion
|
|
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,
|
|
1437
|
+
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, batchCloseDay, countTenderItems, createAttemptReference, createPaymentError, currencyDecimals, currentIntent, defaultReversalFor, definePaymentErrors, derivePaymentStatus, formatPaymentError, groupRedeems, hasRedeemPayment, isBatchCloseDue, isDeliveryFee, isIntentCovered, isMixedTenders, isPaymentError, isRealPaymentMethod, isRedemptionReserved, isScopePaid, isTenderSettled, isTerminalLocked, isTipFee, liveRealTenders, liveTenders, memoryPaymentStorage, mergePaymentTender, missingDeviceFields, money, moneyMax, moneyMin, moneyNumber, moneyNumbers, moneySum, moneyZero, nextIntentStatus, orderRedeems, primaryTender, primaryTenderOf, prorateMinor, realTenders, redeemsCoverTotal, 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.5.0",
|
|
4
4
|
"description": "Fire Spark payments contract: adapters, errors, amounts and policy",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"sideEffects": false,
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
},
|
|
19
19
|
"dependencies": {
|
|
20
20
|
"big.js": "6.2.1",
|
|
21
|
-
"@fire-spark/types": "^0.
|
|
21
|
+
"@fire-spark/types": "^0.5.0"
|
|
22
22
|
},
|
|
23
23
|
"devDependencies": {
|
|
24
24
|
"@types/big.js": "6.2.2",
|