@fire-spark/payments 0.4.1 → 0.6.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.
Files changed (3) hide show
  1. package/dist/index.d.ts +26 -29
  2. package/dist/index.js +103 -156
  3. 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). Amounts are not rounded.
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) => import("big.js").Big;
101
- declare const moneyZero: () => import("big.js").Big;
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
- * Payment redeem as stored in `orders.redeem`. `amount` is the total of
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 PaymentRedeemLike = {
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
- /** Payment redeems of one `payment_id`, ready to seed its tender. */
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
- * Reward (voucher, balance…) paid with a REDEMPTION tender, not a discount (D16):
516
- * it never discounts lines nor has a `redemption_steps` entry.
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 isPaymentRedeem: (redeem: {
519
+ declare const hasRedeemPayment: (redeem: {
519
520
  payment_id?: string | null;
520
521
  }) => boolean;
521
- /** Discount redeems: the ones replayed into `redemption_steps`. */
522
- declare const discountRedeems: <T extends {
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 payment redeem of the order: order-level ones first, then the ones
535
- * kept on a root line (a voucher stored with the product it pays for).
536
- * Both are paid by REDEMPTION tenders; child-line redeems never apply.
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 orderPaymentRedeems: <T extends PaymentRedeemLike>(order: PaymentRedeemSource<T>) => T[];
535
+ declare const orderRedeems: <T extends RedeemLike>(order: RedeemSource<T>) => T[];
539
536
  /**
540
- * Payment redeems pay the whole order, so no real tender is needed: the
541
- * intent is only REDEMPTION tenders and the order advances on its own.
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 paymentRedeemsCoverTotal: (redeem: readonly PaymentRedeemLike[], totals: {
540
+ declare const redeemsCoverTotal: (redeem: readonly RedeemLike[], totals: {
544
541
  total: number;
545
542
  currency: string;
546
543
  }) => boolean;
547
544
  /**
548
- * Groups payment redeems (`payment_id` set) into one entry per
549
- * `payment_id`, in first-appearance order. Discount redeems are skipped.
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 groupPaymentRedeems: (redeem: readonly PaymentRedeemLike[]) => RedemptionGroup[];
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, 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 };
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 coveringTenders(tenders).reduce((sum, tender) => {
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 tendersCoverage(liveTenders(intent)) + amountTolerance(intent.totals.currency) >= total;
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
- const tolerance = amountTolerance(tender.amount.currency);
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
- const drift = lastTotal.minus(last.total);
639
- const subtotal = money(last.subtotal).plus(drift);
640
- const total = moneyNumber(lastTotal);
641
- const next = {
581
+ return [...head, {
642
582
  ...last,
643
- subtotal: moneyNumber(subtotal),
644
- total
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
- ...b1,
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). Amounts are not rounded.
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
- subtotal,
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 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
- };
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
- ...b1,
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
- const gross = money(amount.total).plus(amount.redemptions_applied);
918
- allocation = {
919
- lines,
920
- fees: feesLines,
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
- const drift = primaryTotal.minus(tender.amount.total);
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 gross = money(amount.total).plus(amount.redemptions_applied);
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
- const tolerance = amountTolerance(tender.amount.currency);
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
- * Reward (voucher, balance…) paid with a REDEMPTION tender, not a discount (D16):
1278
- * it never discounts lines nor has a `redemption_steps` entry.
1228
+ * Every redeem has a `payment_id` whose REDEMPTION tender covers it.
1229
+ * Legacy rows stored without one are ignored.
1279
1230
  */
1280
- const isPaymentRedeem = (redeem) => {
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 payment redeem of the order: order-level ones first, then the ones
1289
- * kept on a root line (a voucher stored with the product it pays for).
1290
- * Both are paid by REDEMPTION tenders; child-line redeems never apply.
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 orderPaymentRedeems = (order) => {
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(isPaymentRedeem);
1241
+ return [...order.redeem ?? [], ...onLines].filter(hasRedeemPayment);
1295
1242
  };
1296
1243
  /**
1297
- * Payment redeems pay the whole order, so no real tender is needed: the
1298
- * intent is only REDEMPTION tenders and the order advances on its own.
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 paymentRedeemsCoverTotal = (redeem, totals) => {
1247
+ const redeemsCoverTotal = (redeem, totals) => {
1301
1248
  const paid = redeem.reduce((sum, item) => {
1302
- if (!isPaymentRedeem(item)) return sum;
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 payment redeems (`payment_id` set) into one entry per
1329
- * `payment_id`, in first-appearance order. Discount redeems are skipped.
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 groupPaymentRedeems = (redeem) => {
1278
+ const groupRedeems = (redeem) => {
1332
1279
  const groups = /* @__PURE__ */ new Map();
1333
1280
  for (const item of redeem) {
1334
- if (!isPaymentRedeem(item)) continue;
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, 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 };
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.4.1",
3
+ "version": "0.6.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.4.1"
21
+ "@fire-spark/types": "^0.6.0"
22
22
  },
23
23
  "devDependencies": {
24
24
  "@types/big.js": "6.2.2",