@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.
Files changed (3) hide show
  1. package/dist/index.d.ts +42 -21
  2. package/dist/index.js +314 -211
  3. 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.points_value`). */
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` (minor units) by `units / quantity` per part. Each part
47
- * gets the floor of its exact share; the residual goes one minor unit at a
48
- * time to the largest discarded fraction, ties to the lowest `sequence`.
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
- * prorated per component with the residual minor unit to the largest
57
- * discarded fraction, fees to their tender, tip / delivery and order-level
58
- * adjustments to the primary, then REDEMPTION deducted from the primary
59
- * first and by `sequence`, never below a tender's tip + delivery (D18).
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
- declare const roundTo: (value: number, decimals: number) => number;
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` and `points` are
476
- * totals of the entry (every unit); `quantity` is informational.
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
- * Points / balance paid with a REDEMPTION tender, not a discount (D16):
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 grouped shape, or null for other methods.
533
- * Single-reward details written before grouping read as one reward.
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
- points: number;
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 points are held for the order (L2 / D30). */
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 points are held (D30).
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
- const roundTo = (value, decimals) => {
20
- const factor = 10 ** decimals;
21
- return Math.round((value + Number.EPSILON) * factor) / factor;
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 rounded = roundTo(value, decimals);
70
+ const exact = money(value);
71
+ const scale = money(10).pow(decimals);
72
+ const minor = moneyNumber(exact.times(scale));
27
73
  return {
28
- value: rounded,
29
- minor: Math.round(rounded * 10 ** decimals),
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 amount = toPaymentAmount(input.value * rate, chargeCurrency);
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 points are held for the order (L2 / D30). */
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 points are held (D30).
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
- subtotal: 0,
375
- discount: 0,
376
- tax: 0,
377
- total: 0
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 + right.subtotal,
382
- discount: left.discount + right.discount,
383
- tax: left.tax + right.tax,
384
- total: left.total + right.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` (minor units) by `units / quantity` per part. Each part
427
- * gets the floor of its exact share; the residual goes one minor unit at a
428
- * time to the largest discarded fraction, ties to the lowest `sequence`.
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 shares = parts.map((part, index) => {
432
- const numerator = amount * part.units;
433
- const base = Math.floor(numerator / quantity);
434
- return {
435
- index,
436
- base,
437
- remainder: numerator - base * quantity,
438
- sequence: part.sequence
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 ownMinor = (line, toMinor) => {
589
+ const ownMoney = (line) => {
554
590
  return {
555
- subtotal: toMinor(line.subtotal),
556
- discount: toMinor(line.discount_total),
557
- tax: toMinor(line.tax_total),
558
- total: toMinor(line.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 groupMinor = (line, toMinor) => {
597
+ const groupMoney = (line) => {
562
598
  return {
563
- subtotal: toMinor(line.group_subtotal),
564
- discount: toMinor(line.group_discount),
565
- tax: toMinor(line.group_tax_total),
566
- total: toMinor(line.group_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 Math.abs(value.subtotal - value.discount + value.tax - value.total) <= 1;
606
+ return value.subtotal.minus(value.discount).plus(value.tax).eq(value.total);
571
607
  };
572
- /** Split of one component per part; `tax = total − subtotal + discount`. */
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 s = subtotal[index] ?? 0;
579
- const d = discount[index] ?? 0;
580
- const t = total[index] ?? 0;
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: s,
583
- discount: d,
584
- tax: t - s + d,
585
- total: t
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
- * prorated per component with the residual minor unit to the largest
618
- * discarded fraction, fees to their tender, tip / delivery and order-level
619
- * adjustments to the primary, then REDEMPTION deducted from the primary
620
- * first and by `sequence`, never below a tender's tip + delivery (D18).
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 = toMinor(input.totals.total);
682
+ const orderTotal = money(input.totals.total);
632
683
  const tipDelivery = roots.reduce((sum, line) => {
633
- return isTipFee(line) || isDeliveryFee(line) ? sum + toMinor(line.group_total) : sum;
634
- }, 0);
635
- const redemptionMinor = redemptions.map((tender) => {
636
- return toMinor(tender.redemption_amount ?? 0);
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 = redemptionMinor.reduce((sum, value) => sum + value, 0);
639
- if (redeemed > orderTotal - tipDelivery) return {
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 = fromMinor(redemptionMinor[index] ?? 0);
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 !== orderTotal) {
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 > orderTotal) code = "PAYMENT_REDEMPTION_EXCEEDED";
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
- return {
742
+ const b1 = {
690
743
  products: zeroMinor(),
691
- fees: 0,
692
- tip: 0,
693
- delivery: 0,
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 = fromMinor(value.subtotal);
699
- const discount = fromMinor(value.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: fromMinor(value.tax),
702
- total: fromMinor(value.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 = toMinor(root.group_total);
716
- if (isTipFee(root)) build.tip += total;
717
- else if (isDeliveryFee(root)) build.delivery += total;
718
- else build.fees += total;
719
- const amount = toAmount(root.uid, groupMinor(root, toMinor));
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 = groupMinor(root, toMinor);
790
+ const group = groupMoney(root);
731
791
  build.products = addMinor(build.products, group);
732
792
  const childAmounts = children.map((child) => {
733
- const own = ownMinor(child, toMinor);
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 = ownMinor(component, toMinor);
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 * part.units / root.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
- return sum + build.products.total + build.fees + build.tip + build.delivery;
783
- }, 0);
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 - allocated;
786
- if (adjustment < 0) {
846
+ const adjustment = orderTotal.minus(allocated);
847
+ if (adjustment.lt(0)) {
787
848
  const value = { ...primary.products };
788
- value.discount -= adjustment;
789
- value.total += adjustment;
849
+ value.discount = value.discount.minus(adjustment);
850
+ value.total = value.total.plus(adjustment);
790
851
  primary.products = value;
791
- } else if (adjustment > 0) primary.products = {
852
+ } else if (adjustment.gt(0)) primary.products = {
792
853
  ...primary.products,
793
- total: primary.products.total + adjustment
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 + build.fees + build.tip + build.delivery;
858
+ return build.products.total.plus(build.fees).plus(build.tip).plus(build.delivery);
797
859
  });
798
- if ((totals[0] ?? 0) < 0) return {
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(() => 0);
868
+ const applied = builds.map(() => moneyZero());
807
869
  let pending = redeemed;
808
870
  for (const [index, build] of builds.entries()) {
809
- if (pending <= 0) break;
810
- const floor = build.tip + build.delivery;
811
- const room = Math.max(0, (totals[index] ?? 0) - floor);
812
- const take = Math.min(room, pending);
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 -= take;
876
+ pending = pending.minus(take);
815
877
  }
816
- if (pending > 0) return {
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: [...real.map((tender, index) => {
826
- const build = builds[index];
827
- const total = (totals[index] ?? 0) - (applied[index] ?? 0);
828
- const b1 = {
829
- currency,
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
- ...b1,
842
- ...b2,
843
- ...b3,
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 open = tender.allocation === null;
850
- const lines = open ? [] : tender.allocation?.lines ?? [];
851
- const fees = open ? [] : tender.allocation?.fees ?? [];
852
- let allocation = null;
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
- ...base(tender),
860
- allocation,
861
- amount
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: real[0]?.id ?? null
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 SUM_DECIMALS = 6;
1175
- const add = (a, b) => roundTo(a + b, SUM_DECIMALS);
1273
+ const add = (left, right) => {
1274
+ return moneyNumber(money(left).plus(right));
1275
+ };
1176
1276
  /**
1177
- * Points / balance paid with a REDEMPTION tender, not a discount (D16):
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
- return isPaymentRedeem(item) ? add(sum, item.amount ?? 0) : sum;
1204
- }, 0);
1205
- if (paid <= 0) return false;
1206
- return roundTo(paid, decimals) >= roundTo(totals.total, decimals);
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
- ...r1,
1219
- ...r2
1310
+ reward_id: redeem.reward_id,
1311
+ quantity: redeem.quantity ?? 1,
1312
+ amount: redeem.amount ?? 0
1220
1313
  };
1221
1314
  };
1222
- const detailsOf = (rewards, program, loyalty_transaction_id) => {
1315
+ const detailsOf = (rewards, loyalty_transaction_id) => {
1223
1316
  const d1 = {
1224
1317
  method: REDEMPTION_METHOD,
1225
1318
  rewards,
1226
- points: rewards.reduce((sum, item) => add(sum, item.points), 0),
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 = program === void 0 ? {} : { program };
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 program = items.find((item) => item.program)?.program;
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.points_value,
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 grouped shape, or null for other methods.
1265
- * Single-reward details written before grouping read as one reward.
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
- if (Array.isArray(details.rewards)) return details;
1270
- if (typeof details.reward_id !== "string") return null;
1271
- const legacy = details;
1272
- const r1 = {
1273
- reward_id: legacy.reward_id,
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
- const r2 = { points: legacy.points ?? 0 };
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: 0,
1332
- tax: 0
1433
+ base: moneyZero(),
1434
+ tax: moneyZero()
1333
1435
  };
1334
- current.base += line.subtotal - line.discount_total;
1335
- current.tax += line.tax_total;
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: roundTo(sums.base * share, decimals),
1342
- tax: roundTo(sums.tax * share, decimals)
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.5",
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
- "@fire-spark/types": "^0.3.5"
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"