@wtree/payload-ecommerce-coupon 3.78.8 → 3.78.10

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.mjs CHANGED
@@ -459,102 +459,88 @@ const createReferralProgramsCollection = (pluginConfig) => {
459
459
  update: access.isAdmin || (() => false),
460
460
  delete: access.isAdmin || (() => false)
461
461
  },
462
- hooks: {
463
- beforeChange: [({ data }) => {
464
- if (!data.commissionRules || !Array.isArray(data.commissionRules) || data.commissionRules.length === 0) throw new APIError("At least one commission rule is required", 400);
465
- const rawMaxPartner = toNumber(data.maxPartnerCommissionPerOrder);
466
- if (rawMaxPartner != null && rawMaxPartner < 0) throw new APIError("Maximum commission per order for partner must be a non-negative number", 400);
467
- const rawMaxCustomer = toNumber(data.maxCustomerDiscountPerOrder);
468
- if (rawMaxCustomer != null && rawMaxCustomer < 0) throw new APIError("Maximum discount for customer per order must be a non-negative number", 400);
469
- const rawMinOrder = toNumber(data.minOrderAmount);
470
- if (rawMinOrder != null && rawMinOrder < 0) throw new APIError("Minimum Order Amount must be a non-negative number", 400);
471
- data.maxPartnerCommissionPerOrder = rawMaxPartner != null ? Math.round(rawMaxPartner * 100) : null;
472
- data.maxCustomerDiscountPerOrder = rawMaxCustomer != null ? Math.round(rawMaxCustomer * 100) : null;
473
- data.minOrderAmount = rawMinOrder != null ? Math.round(rawMinOrder * 100) : null;
474
- data.commissionRules = data.commissionRules.map((rule, index) => {
475
- const r = rule;
476
- if (!r.totalCommission) throw new APIError(`Commission rule ${index + 1}: Total Commission is required`, 400);
477
- if (!r.totalCommission.type || !allowedTotalCommissionTypes.includes(r.totalCommission.type)) throw new APIError(`Commission rule ${index + 1}: Total Commission type must be one of ${allowedTotalCommissionTypes.join(", ")}`, 400);
478
- const type = r.totalCommission.type;
479
- const appliesTo = r.appliesTo ?? "all";
480
- if (appliesTo === "products" && (!r.products || r.products.length === 0)) throw new APIError(`Commission rule ${index + 1}: At least one product is required`, 400);
481
- if ((appliesTo === "segments" || appliesTo === "categories") && (!r.categories || r.categories.length === 0) && (!r.tags || r.tags.length === 0)) throw new APIError(`Commission rule ${index + 1}: At least one category or tag is required`, 400);
482
- let partnerSplit;
483
- let customerSplit;
484
- let partnerPercent = null;
485
- let customerPercent = null;
486
- let partnerAmount = null;
487
- let customerAmount = null;
488
- let splitWarning = null;
489
- if (type === "percentage") {
490
- const partnerPctInput = toNumber(r.partnerPercent) ?? toNumber(r.partnerSplit);
491
- const customerPctInput = toNumber(r.customerPercent) ?? toNumber(r.customerSplit);
492
- if (partnerPctInput == null || partnerPctInput < 0 || partnerPctInput > 100) throw new APIError(`Commission rule ${index + 1}: Partner Split must be between 0 and 100`, 400);
493
- if (customerPctInput != null && (customerPctInput < 0 || customerPctInput > 100)) throw new APIError(`Commission rule ${index + 1}: Customer percentage must be between 0 and 100`, 400);
494
- const customerPctComputed = customerPctInput != null ? customerPctInput : 100 - partnerPctInput;
495
- const percentTotal = partnerPctInput + customerPctComputed;
496
- if (percentTotal > 100) throw new APIError(`Commission rule ${index + 1}: Partner percentage + Customer percentage cannot exceed 100`, 400);
497
- if (percentTotal > 50) splitWarning = `High total split configured: ${percentTotal}% (partner + customer).`;
498
- partnerPercent = partnerPctInput;
499
- customerPercent = customerPctComputed;
500
- partnerSplit = partnerPctInput;
501
- customerSplit = customerPctComputed;
502
- } else {
503
- const partnerAmountInput = toNumber(r.partnerAmount);
504
- const customerAmountInput = toNumber(r.customerAmount);
505
- const legacyPartnerSplitInput = toNumber(r.partnerSplit);
506
- const legacyCustomerSplitInput = toNumber(r.customerSplit);
507
- const hasNewFixedInputs = partnerAmountInput != null || customerAmountInput != null;
508
- const hasLegacyFixedInputs = legacyPartnerSplitInput != null || legacyCustomerSplitInput != null;
509
- if (hasNewFixedInputs) {
510
- if (partnerAmountInput == null || partnerAmountInput < 0) throw new APIError(`Commission rule ${index + 1}: Partner fixed amount must be a non-negative number`, 400);
511
- if (customerAmountInput == null || customerAmountInput < 0) throw new APIError(`Commission rule ${index + 1}: Customer fixed amount must be a non-negative number`, 400);
512
- partnerAmount = partnerAmountInput;
513
- customerAmount = customerAmountInput;
514
- partnerSplit = partnerAmountInput;
515
- customerSplit = customerAmountInput;
516
- } else if (hasLegacyFixedInputs) {
517
- if (legacyPartnerSplitInput == null || legacyPartnerSplitInput < 0) throw new APIError(`Commission rule ${index + 1}: For fixed commissions, both partner and customer values must be non-negative numbers`, 400);
518
- const resolvedLegacyCustomerSplit = legacyCustomerSplitInput ?? 100 - legacyPartnerSplitInput;
519
- if (resolvedLegacyCustomerSplit == null || resolvedLegacyCustomerSplit < 0) throw new APIError(`Commission rule ${index + 1}: For fixed commissions, both partner and customer values must be non-negative numbers`, 400);
520
- partnerSplit = legacyPartnerSplitInput;
521
- customerSplit = resolvedLegacyCustomerSplit;
522
- partnerAmount = null;
523
- customerAmount = null;
524
- } else throw new APIError(`Commission rule ${index + 1}: For fixed commissions, both partner and customer values must be provided`, 400);
525
- }
526
- return {
527
- ...rule,
528
- appliesTo: appliesTo === "categories" ? "segments" : appliesTo,
529
- totalCommission: {
530
- type,
531
- ...typeof r.totalCommission.value === "number" ? { value: r.totalCommission.value } : {},
532
- ...typeof r.totalCommission.maxAmount === "number" ? { maxAmount: r.totalCommission.maxAmount } : {}
533
- },
534
- partnerPercent,
535
- customerPercent,
536
- partnerAmount,
537
- customerAmount,
538
- partnerSplit,
539
- customerSplit,
540
- splitWarning
541
- };
542
- });
543
- return data;
544
- }],
545
- afterRead: [({ doc }) => {
546
- if (!doc) return doc;
547
- const unscale = (value) => {
548
- const n = toNumber(value);
549
- if (n == null) return null;
550
- return Math.round(n / 100 * 100) / 100;
462
+ hooks: { beforeChange: [({ data }) => {
463
+ if (!data.commissionRules || !Array.isArray(data.commissionRules) || data.commissionRules.length === 0) throw new APIError("At least one commission rule is required", 400);
464
+ const rawMaxPartner = toNumber(data.maxPartnerCommissionPerOrder);
465
+ if (rawMaxPartner != null && rawMaxPartner < 0) throw new APIError("Maximum commission per order for partner must be a non-negative number", 400);
466
+ const rawMaxCustomer = toNumber(data.maxCustomerDiscountPerOrder);
467
+ if (rawMaxCustomer != null && rawMaxCustomer < 0) throw new APIError("Maximum discount for customer per order must be a non-negative number", 400);
468
+ const rawMinOrder = toNumber(data.minOrderAmount);
469
+ if (rawMinOrder != null && rawMinOrder < 0) throw new APIError("Minimum Order Amount must be a non-negative number", 400);
470
+ data.maxPartnerCommissionPerOrder = rawMaxPartner ?? null;
471
+ data.maxCustomerDiscountPerOrder = rawMaxCustomer ?? null;
472
+ data.minOrderAmount = rawMinOrder ?? null;
473
+ data.commissionRules = data.commissionRules.map((rule, index) => {
474
+ const r = rule;
475
+ if (!r.totalCommission) throw new APIError(`Commission rule ${index + 1}: Total Commission is required`, 400);
476
+ if (!r.totalCommission.type || !allowedTotalCommissionTypes.includes(r.totalCommission.type)) throw new APIError(`Commission rule ${index + 1}: Total Commission type must be one of ${allowedTotalCommissionTypes.join(", ")}`, 400);
477
+ const type = r.totalCommission.type;
478
+ const appliesTo = r.appliesTo ?? "all";
479
+ if (appliesTo === "products" && (!r.products || r.products.length === 0)) throw new APIError(`Commission rule ${index + 1}: At least one product is required`, 400);
480
+ if ((appliesTo === "segments" || appliesTo === "categories") && (!r.categories || r.categories.length === 0) && (!r.tags || r.tags.length === 0)) throw new APIError(`Commission rule ${index + 1}: At least one category or tag is required`, 400);
481
+ let partnerSplit;
482
+ let customerSplit;
483
+ let partnerPercent = null;
484
+ let customerPercent = null;
485
+ let partnerAmount = null;
486
+ let customerAmount = null;
487
+ let splitWarning = null;
488
+ if (type === "percentage") {
489
+ const partnerPctInput = toNumber(r.partnerPercent) ?? toNumber(r.partnerSplit);
490
+ const customerPctInput = toNumber(r.customerPercent) ?? toNumber(r.customerSplit);
491
+ if (partnerPctInput == null || partnerPctInput < 0 || partnerPctInput > 100) throw new APIError(`Commission rule ${index + 1}: Partner Split must be between 0 and 100`, 400);
492
+ if (customerPctInput != null && (customerPctInput < 0 || customerPctInput > 100)) throw new APIError(`Commission rule ${index + 1}: Customer percentage must be between 0 and 100`, 400);
493
+ const customerPctComputed = customerPctInput != null ? customerPctInput : 100 - partnerPctInput;
494
+ const percentTotal = partnerPctInput + customerPctComputed;
495
+ if (percentTotal > 100) throw new APIError(`Commission rule ${index + 1}: Partner percentage + Customer percentage cannot exceed 100`, 400);
496
+ if (percentTotal > 50) splitWarning = `High total split configured: ${percentTotal}% (partner + customer).`;
497
+ partnerPercent = partnerPctInput;
498
+ customerPercent = customerPctComputed;
499
+ partnerSplit = partnerPctInput;
500
+ customerSplit = customerPctComputed;
501
+ } else {
502
+ const partnerAmountInput = toNumber(r.partnerAmount);
503
+ const customerAmountInput = toNumber(r.customerAmount);
504
+ const legacyPartnerSplitInput = toNumber(r.partnerSplit);
505
+ const legacyCustomerSplitInput = toNumber(r.customerSplit);
506
+ const hasNewFixedInputs = partnerAmountInput != null || customerAmountInput != null;
507
+ const hasLegacyFixedInputs = legacyPartnerSplitInput != null || legacyCustomerSplitInput != null;
508
+ if (hasNewFixedInputs) {
509
+ if (partnerAmountInput == null || partnerAmountInput < 0) throw new APIError(`Commission rule ${index + 1}: Partner fixed amount must be a non-negative number`, 400);
510
+ if (customerAmountInput == null || customerAmountInput < 0) throw new APIError(`Commission rule ${index + 1}: Customer fixed amount must be a non-negative number`, 400);
511
+ partnerAmount = partnerAmountInput;
512
+ customerAmount = customerAmountInput;
513
+ partnerSplit = partnerAmountInput;
514
+ customerSplit = customerAmountInput;
515
+ } else if (hasLegacyFixedInputs) {
516
+ if (legacyPartnerSplitInput == null || legacyPartnerSplitInput < 0) throw new APIError(`Commission rule ${index + 1}: For fixed commissions, both partner and customer values must be non-negative numbers`, 400);
517
+ const resolvedLegacyCustomerSplit = legacyCustomerSplitInput ?? 100 - legacyPartnerSplitInput;
518
+ if (resolvedLegacyCustomerSplit == null || resolvedLegacyCustomerSplit < 0) throw new APIError(`Commission rule ${index + 1}: For fixed commissions, both partner and customer values must be non-negative numbers`, 400);
519
+ partnerSplit = legacyPartnerSplitInput;
520
+ customerSplit = resolvedLegacyCustomerSplit;
521
+ partnerAmount = null;
522
+ customerAmount = null;
523
+ } else throw new APIError(`Commission rule ${index + 1}: For fixed commissions, both partner and customer values must be provided`, 400);
524
+ }
525
+ return {
526
+ ...rule,
527
+ appliesTo: appliesTo === "categories" ? "segments" : appliesTo,
528
+ totalCommission: {
529
+ type,
530
+ ...typeof r.totalCommission.value === "number" ? { value: r.totalCommission.value } : {},
531
+ ...typeof r.totalCommission.maxAmount === "number" ? { maxAmount: r.totalCommission.maxAmount } : {}
532
+ },
533
+ partnerPercent,
534
+ customerPercent,
535
+ partnerAmount,
536
+ customerAmount,
537
+ partnerSplit,
538
+ customerSplit,
539
+ splitWarning
551
540
  };
552
- doc.maxPartnerCommissionPerOrder = unscale(doc.maxPartnerCommissionPerOrder);
553
- doc.maxCustomerDiscountPerOrder = unscale(doc.maxCustomerDiscountPerOrder);
554
- doc.minOrderAmount = unscale(doc.minOrderAmount);
555
- return doc;
556
- }]
557
- },
541
+ });
542
+ return data;
543
+ }] },
558
544
  fields: [
559
545
  {
560
546
  name: "name",
@@ -720,14 +706,6 @@ const createReferralProgramsCollection = (pluginConfig) => {
720
706
  timestamps: true
721
707
  };
722
708
  };
723
- //#endregion
724
- //#region src/utilities/roundTo2.ts
725
- /**
726
- * Rounds a number to 2 decimal places (standard for monetary values).
727
- */
728
- function roundTo2(value) {
729
- return Math.round(value * 100) / 100;
730
- }
731
709
  function normalizeCurrencyCode(currencyCode) {
732
710
  if (!currencyCode) return "AED";
733
711
  return currencyCode.toUpperCase();
@@ -763,16 +741,35 @@ function getCartItemUnitPrice({ item, product, variant, currencyCode, defaultCur
763
741
  }
764
742
  //#endregion
765
743
  //#region src/utilities/calculateValues.ts
744
+ /** Convert a normal-currency amount to integer cents. */
745
+ function toCents(amount) {
746
+ return Math.round(amount * 100);
747
+ }
748
+ /** Convert integer cents back to a normal-currency amount (2 dp max). */
749
+ function fromCents(cents) {
750
+ return Math.round(cents) / 100;
751
+ }
752
+ /**
753
+ * Calculate the discount amount for a coupon.
754
+ *
755
+ * @param coupon - Coupon document from DB (values in normal currency).
756
+ * @param cartTotal - Cart subtotal in normal currency.
757
+ * @returns Discount amount in normal currency (2 dp).
758
+ */
766
759
  function calculateCouponDiscount({ coupon, cartTotal }) {
767
- let discount = 0;
760
+ const cartCents = toCents(cartTotal);
761
+ let discountCents = 0;
768
762
  if (coupon.type === "percentage") {
769
- discount = roundTo2(cartTotal * coupon.value / 100);
770
- if (coupon.maxDiscountAmount != null && discount > coupon.maxDiscountAmount) discount = roundTo2(coupon.maxDiscountAmount);
763
+ discountCents = Math.floor(cartCents * coupon.value / 100);
764
+ if (coupon.maxDiscountAmount != null) {
765
+ const maxCents = toCents(coupon.maxDiscountAmount);
766
+ if (discountCents > maxCents) discountCents = maxCents;
767
+ }
771
768
  } else if (coupon.type === "fixed") {
772
- discount = roundTo2(coupon.value);
773
- if (discount > cartTotal) discount = roundTo2(cartTotal);
769
+ discountCents = toCents(coupon.value);
770
+ if (discountCents > cartCents) discountCents = cartCents;
774
771
  }
775
- return roundTo2(discount);
772
+ return fromCents(discountCents);
776
773
  }
777
774
  function relationId$5(value) {
778
775
  if (value == null) return null;
@@ -793,19 +790,25 @@ function getRuleSplits(rule) {
793
790
  customerSplit: typeof rule.customerSplit === "number" ? rule.customerSplit : typeof rule.refereeSplit === "number" ? rule.refereeSplit : 100 - partnerRaw
794
791
  };
795
792
  }
796
- function calculateItemRewardByRule({ rule, itemTotal, quantity, allowedTotalCommissionTypes }) {
793
+ /**
794
+ * Calculate partner and customer reward for a single line item.
795
+ *
796
+ * ALL inputs are expected in CENTS.
797
+ * Returns rewards in CENTS, or null if the rule is inapplicable.
798
+ */
799
+ function calculateItemRewardByRule({ rule, itemTotalCents, quantity, allowedTotalCommissionTypes }) {
797
800
  const allowedTypes = allowedCommissionTypesSet(allowedTotalCommissionTypes);
798
801
  if (rule.totalCommission) {
799
802
  if (!allowedTypes.has(rule.totalCommission.type)) return null;
800
- const resolvedMaxAmount = typeof rule.totalCommission.maxAmount === "number" && Number.isFinite(rule.totalCommission.maxAmount) ? rule.totalCommission.maxAmount : null;
803
+ const resolvedMaxAmountCents = typeof rule.totalCommission.maxAmount === "number" && Number.isFinite(rule.totalCommission.maxAmount) ? toCents(rule.totalCommission.maxAmount) : null;
801
804
  if (rule.totalCommission.type === "fixed" && rule.totalCommission.value == null) {
802
- const partnerAmtPerUnit = typeof rule.partnerSplit === "number" ? rule.partnerSplit : null;
803
- const customerAmtPerUnit = typeof rule.customerSplit === "number" ? rule.customerSplit : null;
804
- if (partnerAmtPerUnit == null || customerAmtPerUnit == null) return null;
805
- let partner = partnerAmtPerUnit * quantity;
806
- let customer = customerAmtPerUnit * quantity;
807
- if (resolvedMaxAmount != null) {
808
- const maxPotForLine = resolvedMaxAmount * quantity;
805
+ const partnerAmtPerUnitCents = typeof rule.partnerSplit === "number" ? toCents(rule.partnerSplit) : null;
806
+ const customerAmtPerUnitCents = typeof rule.customerSplit === "number" ? toCents(rule.customerSplit) : null;
807
+ if (partnerAmtPerUnitCents == null || customerAmtPerUnitCents == null) return null;
808
+ let partner = partnerAmtPerUnitCents * quantity;
809
+ let customer = customerAmtPerUnitCents * quantity;
810
+ if (resolvedMaxAmountCents != null) {
811
+ const maxPotForLine = resolvedMaxAmountCents * quantity;
809
812
  const totalPot = partner + customer;
810
813
  if (totalPot > maxPotForLine && totalPot > 0) {
811
814
  const ratio = maxPotForLine / totalPot;
@@ -818,7 +821,6 @@ function calculateItemRewardByRule({ rule, itemTotal, quantity, allowedTotalComm
818
821
  customer
819
822
  };
820
823
  }
821
- let totalPot = 0;
822
824
  if (rule.totalCommission.type === "percentage") {
823
825
  const commissionValue = typeof rule.totalCommission.value === "number" && Number.isFinite(rule.totalCommission.value) ? rule.totalCommission.value : null;
824
826
  if (commissionValue == null) {
@@ -826,58 +828,63 @@ function calculateItemRewardByRule({ rule, itemTotal, quantity, allowedTotalComm
826
828
  if (partnerPercentInput == null || partnerPercentInput < 0 || partnerPercentInput > 100) return null;
827
829
  const customerPercentInput = typeof rule.customerPercent === "number" ? rule.customerPercent : typeof rule.customerSplit === "number" ? rule.customerSplit : 100 - partnerPercentInput;
828
830
  if (customerPercentInput == null || customerPercentInput < 0 || customerPercentInput > 100) return null;
829
- const partner = itemTotal * partnerPercentInput / 100;
830
- const customer = itemTotal * customerPercentInput / 100;
831
- if (resolvedMaxAmount != null) {
832
- const maxPotForLine = resolvedMaxAmount * quantity;
831
+ let partner = Math.floor(itemTotalCents * partnerPercentInput / 100);
832
+ let customer = Math.floor(itemTotalCents * customerPercentInput / 100);
833
+ if (resolvedMaxAmountCents != null) {
834
+ const maxPotForLine = resolvedMaxAmountCents * quantity;
833
835
  const totalForLine = partner + customer;
834
836
  if (totalForLine > maxPotForLine && totalForLine > 0) {
835
837
  const ratio = maxPotForLine / totalForLine;
836
- return {
837
- partner: Math.floor(partner * ratio),
838
- customer: Math.floor(customer * ratio)
839
- };
838
+ partner = Math.floor(partner * ratio);
839
+ customer = Math.floor(customer * ratio);
840
840
  }
841
841
  }
842
842
  return {
843
- partner: Math.floor(partner),
844
- customer: Math.floor(customer)
843
+ partner,
844
+ customer
845
845
  };
846
846
  }
847
- totalPot = itemTotal * commissionValue / 100;
848
- } else {
847
+ let totalPotCents = Math.floor(itemTotalCents * commissionValue / 100);
848
+ if (resolvedMaxAmountCents != null) {
849
+ const maxPotForLine = resolvedMaxAmountCents * quantity;
850
+ if (totalPotCents > maxPotForLine) totalPotCents = maxPotForLine;
851
+ }
849
852
  const splits = getRuleSplits(rule);
850
853
  if (!splits) return null;
851
- totalPot = rule.totalCommission.value * quantity;
852
- if (resolvedMaxAmount != null) {
853
- const maxPotForLine = resolvedMaxAmount * quantity;
854
- if (totalPot > maxPotForLine) totalPot = maxPotForLine;
855
- }
856
854
  return {
857
- partner: Math.floor(totalPot * splits.partnerSplit / 100),
858
- customer: Math.floor(totalPot * splits.customerSplit / 100)
855
+ partner: Math.floor(totalPotCents * splits.partnerSplit / 100),
856
+ customer: Math.floor(totalPotCents * splits.customerSplit / 100)
859
857
  };
860
858
  }
861
- if (resolvedMaxAmount != null) {
862
- const maxPotForLine = resolvedMaxAmount * quantity;
863
- if (totalPot > maxPotForLine) totalPot = maxPotForLine;
859
+ {
860
+ const splits = getRuleSplits(rule);
861
+ if (!splits) return null;
862
+ let totalPotCents = toCents(rule.totalCommission.value) * quantity;
863
+ if (resolvedMaxAmountCents != null) {
864
+ const maxPotForLine = resolvedMaxAmountCents * quantity;
865
+ if (totalPotCents > maxPotForLine) totalPotCents = maxPotForLine;
866
+ }
867
+ return {
868
+ partner: Math.floor(totalPotCents * splits.partnerSplit / 100),
869
+ customer: Math.floor(totalPotCents * splits.customerSplit / 100)
870
+ };
864
871
  }
865
- const splits = getRuleSplits(rule);
866
- if (!splits) return null;
867
- return {
868
- partner: Math.floor(totalPot * splits.partnerSplit / 100),
869
- customer: Math.floor(totalPot * splits.customerSplit / 100)
870
- };
871
872
  }
872
873
  if (rule.referrerReward && rule.refereeReward) {
873
874
  let partner = 0;
874
- if (rule.referrerReward.type === "percentage") partner = itemTotal * rule.referrerReward.value / 100;
875
- else partner = rule.referrerReward.value * quantity;
876
- if (rule.referrerReward.maxReward != null && partner > rule.referrerReward.maxReward) partner = rule.referrerReward.maxReward;
875
+ if (rule.referrerReward.type === "percentage") partner = Math.floor(itemTotalCents * rule.referrerReward.value / 100);
876
+ else partner = toCents(rule.referrerReward.value) * quantity;
877
+ if (rule.referrerReward.maxReward != null) {
878
+ const maxCents = toCents(rule.referrerReward.maxReward);
879
+ if (partner > maxCents) partner = maxCents;
880
+ }
877
881
  let customer = 0;
878
- if (rule.refereeReward.type === "percentage") customer = itemTotal * rule.refereeReward.value / 100;
879
- else customer = rule.refereeReward.value * quantity;
880
- if (rule.refereeReward.maxReward != null && customer > rule.refereeReward.maxReward) customer = rule.refereeReward.maxReward;
882
+ if (rule.refereeReward.type === "percentage") customer = Math.floor(itemTotalCents * rule.refereeReward.value / 100);
883
+ else customer = toCents(rule.refereeReward.value) * quantity;
884
+ if (rule.refereeReward.maxReward != null) {
885
+ const maxCents = toCents(rule.refereeReward.maxReward);
886
+ if (customer > maxCents) customer = maxCents;
887
+ }
881
888
  return {
882
889
  partner,
883
890
  customer
@@ -893,12 +900,12 @@ function getItemCategoryIds(item) {
893
900
  function getItemTagIds(item) {
894
901
  return Array.isArray(item?.product?.tags) ? normalizeIds(item.product.tags) : [];
895
902
  }
896
- function selectBestRuleForItem({ rules, item, itemTotal, quantity, cartTotal, minOrderAmount, allowedTotalCommissionTypes }) {
903
+ function selectBestRuleForItem({ rules, item, itemTotalCents, quantity, cartTotalCents, minOrderAmountCents, allowedTotalCommissionTypes }) {
897
904
  const allowedTypes = allowedCommissionTypesSet(allowedTotalCommissionTypes);
898
905
  const eligibleRules = rules.filter((rule) => {
899
906
  if (!(rule?.totalCommission?.type ? allowedTypes.has(rule.totalCommission.type) : true)) return false;
900
- const resolvedMinOrderAmount = typeof minOrderAmount === "number" && Number.isFinite(minOrderAmount) ? minOrderAmount : typeof rule?.minOrderAmount === "number" && Number.isFinite(rule.minOrderAmount) ? rule.minOrderAmount : null;
901
- if (resolvedMinOrderAmount != null) return cartTotal >= resolvedMinOrderAmount;
907
+ const resolvedMinCents = minOrderAmountCents != null && Number.isFinite(minOrderAmountCents) ? minOrderAmountCents : typeof rule?.minOrderAmount === "number" && Number.isFinite(rule.minOrderAmount) ? toCents(rule.minOrderAmount) : null;
908
+ if (resolvedMinCents != null) return cartTotalCents >= resolvedMinCents;
902
909
  return true;
903
910
  });
904
911
  const productId = relationId$5(item.product);
@@ -921,7 +928,7 @@ function selectBestRuleForItem({ rules, item, itemTotal, quantity, cartTotal, mi
921
928
  for (const rule of candidates) {
922
929
  const reward = calculateItemRewardByRule({
923
930
  rule,
924
- itemTotal,
931
+ itemTotalCents,
925
932
  quantity,
926
933
  allowedTotalCommissionTypes
927
934
  });
@@ -947,6 +954,10 @@ function selectBestRuleForItem({ rules, item, itemTotal, quantity, cartTotal, mi
947
954
  }
948
955
  return best;
949
956
  }
957
+ /**
958
+ * Returns the effective minimum order amount for a program in NORMAL CURRENCY.
959
+ * Returns null if there is no minimum.
960
+ */
950
961
  function getProgramMinimumOrderAmount({ program, allowedTotalCommissionTypes }) {
951
962
  if (typeof program?.minOrderAmount === "number" && Number.isFinite(program.minOrderAmount)) return program.minOrderAmount;
952
963
  const rules = Array.isArray(program?.commissionRules) ? program.commissionRules : [];
@@ -959,50 +970,66 @@ function getProgramMinimumOrderAmount({ program, allowedTotalCommissionTypes })
959
970
  if (!minValues.length) return null;
960
971
  return Math.min(...minValues);
961
972
  }
973
+ /**
974
+ * Calculate total partner commission and customer discount for a cart.
975
+ *
976
+ * All monetary inputs are in NORMAL CURRENCY.
977
+ * Returns results in NORMAL CURRENCY (2 dp).
978
+ */
962
979
  function calculateCommissionAndDiscount({ cartItems, program, currencyCode = "AED", cartTotal = 0, allowedTotalCommissionTypes }) {
963
980
  const rules = Array.isArray(program?.commissionRules) ? program.commissionRules : [];
964
981
  if (!rules.length) return {
965
982
  partnerCommission: 0,
966
983
  customerDiscount: 0
967
984
  };
968
- let totalPartnerCommission = 0;
969
- let totalCustomerDiscount = 0;
985
+ const cartTotalCents = toCents(cartTotal);
986
+ const programMinOrderAmountCents = typeof program?.minOrderAmount === "number" && Number.isFinite(program.minOrderAmount) ? toCents(program.minOrderAmount) : null;
987
+ let totalPartnerCents = 0;
988
+ let totalCustomerCents = 0;
970
989
  for (const item of cartItems) {
971
990
  const product = typeof item.product === "object" ? item.product : {};
972
- const itemPrice = getCartItemUnitPrice({
991
+ const itemPriceCurrency = getCartItemUnitPrice({
973
992
  item,
974
993
  product,
975
994
  variant: typeof item.variant === "object" ? item.variant : {},
976
995
  currencyCode
977
996
  });
978
997
  const quantity = item.quantity ?? 1;
979
- const itemTotal = itemPrice * quantity;
998
+ const itemTotalCents = toCents(itemPriceCurrency) * quantity;
980
999
  const bestMatch = selectBestRuleForItem({
981
1000
  rules,
982
1001
  item: {
983
1002
  ...item,
984
1003
  product
985
1004
  },
986
- itemTotal,
1005
+ itemTotalCents,
987
1006
  quantity,
988
- cartTotal,
989
- minOrderAmount: typeof program?.minOrderAmount === "number" && Number.isFinite(program.minOrderAmount) ? program.minOrderAmount : null,
1007
+ cartTotalCents,
1008
+ minOrderAmountCents: programMinOrderAmountCents,
990
1009
  allowedTotalCommissionTypes
991
1010
  });
992
1011
  if (!bestMatch) continue;
993
- totalPartnerCommission += bestMatch.reward.partner;
994
- totalCustomerDiscount += bestMatch.reward.customer;
1012
+ totalPartnerCents += bestMatch.reward.partner;
1013
+ totalCustomerCents += bestMatch.reward.customer;
995
1014
  }
996
- const maxPartnerCommissionPerOrder = typeof program?.maxPartnerCommissionPerOrder === "number" && Number.isFinite(program.maxPartnerCommissionPerOrder) ? program.maxPartnerCommissionPerOrder : null;
997
- const maxCustomerDiscountPerOrder = typeof program?.maxCustomerDiscountPerOrder === "number" && Number.isFinite(program.maxCustomerDiscountPerOrder) ? program.maxCustomerDiscountPerOrder : null;
998
- if (maxPartnerCommissionPerOrder != null) totalPartnerCommission = Math.min(totalPartnerCommission, maxPartnerCommissionPerOrder);
999
- if (maxCustomerDiscountPerOrder != null) totalCustomerDiscount = Math.min(totalCustomerDiscount, maxCustomerDiscountPerOrder);
1015
+ const maxPartnerCents = typeof program?.maxPartnerCommissionPerOrder === "number" && Number.isFinite(program.maxPartnerCommissionPerOrder) ? toCents(program.maxPartnerCommissionPerOrder) : null;
1016
+ const maxCustomerCents = typeof program?.maxCustomerDiscountPerOrder === "number" && Number.isFinite(program.maxCustomerDiscountPerOrder) ? toCents(program.maxCustomerDiscountPerOrder) : null;
1017
+ if (maxPartnerCents != null) totalPartnerCents = Math.min(totalPartnerCents, maxPartnerCents);
1018
+ if (maxCustomerCents != null) totalCustomerCents = Math.min(totalCustomerCents, maxCustomerCents);
1000
1019
  return {
1001
- partnerCommission: totalPartnerCommission,
1002
- customerDiscount: totalCustomerDiscount
1020
+ partnerCommission: fromCents(totalPartnerCents),
1021
+ customerDiscount: fromCents(totalCustomerCents)
1003
1022
  };
1004
1023
  }
1005
1024
  //#endregion
1025
+ //#region src/utilities/roundTo2.ts
1026
+ /**
1027
+ * Rounds a number to 2 decimal places (standard for monetary values).
1028
+ */
1029
+ function roundTo2(value) {
1030
+ return Math.round(value * 100) / 100;
1031
+ }
1032
+ //#endregion
1006
1033
  //#region src/endpoints/applyCoupon.ts
1007
1034
  function relationId$4(value) {
1008
1035
  if (value == null) return null;