@wtree/payload-ecommerce-coupon 3.78.7 → 3.78.9

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.js CHANGED
@@ -423,23 +423,21 @@ const createReferralCodesCollection = (pluginConfig) => {
423
423
  };
424
424
  };
425
425
  //#endregion
426
- //#region src/utilities/roundTo2.ts
427
- /**
428
- * Rounds a number to 2 decimal places (standard for monetary values).
429
- */
430
- function roundTo2(value) {
431
- return Math.round(value * 100) / 100;
432
- }
433
- //#endregion
434
426
  //#region src/collections/createReferralProgramsCollection.ts
435
427
  function toNumber(value) {
436
428
  return typeof value === "number" && Number.isFinite(value) ? value : null;
437
429
  }
438
- function normalizeLegacyX100Money(value) {
439
- if (value == null) return null;
440
- if (Number.isInteger(value) && Math.abs(value) >= 1e3) return roundTo2(value / 100);
441
- return value;
442
- }
430
+ /**
431
+ * Scaling policy:
432
+ * - Admin inputs normal currency values (e.g. 100 means $100).
433
+ * - Internally, maxPartnerCommissionPerOrder, maxCustomerDiscountPerOrder, and
434
+ * minOrderAmount are stored in x100 (integer cents) form to avoid floating-point
435
+ * drift on monetary cap fields.
436
+ * - beforeChange → multiply by 100 before persisting.
437
+ * - afterRead → divide by 100 before returning to admin / calculation code.
438
+ * - partnerAmount / customerAmount per-rule fixed amounts are NOT scaled here;
439
+ * they represent per-item fixed currency amounts and are stored as-is.
440
+ */
443
441
  const createReferralProgramsCollection = (pluginConfig) => {
444
442
  const { collections, access, adminGroups, referralConfig, integration } = pluginConfig;
445
443
  const allowedTotalCommissionTypes = referralConfig.allowedTotalCommissionTypes;
@@ -462,88 +460,102 @@ const createReferralProgramsCollection = (pluginConfig) => {
462
460
  update: access.isAdmin || (() => false),
463
461
  delete: access.isAdmin || (() => false)
464
462
  },
465
- hooks: { beforeChange: [({ data }) => {
466
- if (!data.commissionRules || !Array.isArray(data.commissionRules) || data.commissionRules.length === 0) throw new payload.APIError("At least one commission rule is required", 400);
467
- const maxPartnerCommissionPerOrder = normalizeLegacyX100Money(toNumber(data.maxPartnerCommissionPerOrder));
468
- if (maxPartnerCommissionPerOrder != null && maxPartnerCommissionPerOrder < 0) throw new payload.APIError("Maximum commission per order for partner must be a non-negative number", 400);
469
- const maxCustomerDiscountPerOrder = normalizeLegacyX100Money(toNumber(data.maxCustomerDiscountPerOrder));
470
- if (maxCustomerDiscountPerOrder != null && maxCustomerDiscountPerOrder < 0) throw new payload.APIError("Maximum discount for customer per order must be a non-negative number", 400);
471
- const minOrderAmount = normalizeLegacyX100Money(toNumber(data.minOrderAmount));
472
- if (minOrderAmount != null && minOrderAmount < 0) throw new payload.APIError("Minimum Order Amount must be a non-negative number", 400);
473
- data.maxPartnerCommissionPerOrder = maxPartnerCommissionPerOrder != null ? maxPartnerCommissionPerOrder : null;
474
- data.maxCustomerDiscountPerOrder = maxCustomerDiscountPerOrder != null ? maxCustomerDiscountPerOrder : null;
475
- data.minOrderAmount = minOrderAmount ?? null;
476
- data.commissionRules = data.commissionRules.map((rule, index) => {
477
- const r = rule;
478
- if (!r.totalCommission) throw new payload.APIError(`Commission rule ${index + 1}: Total Commission is required`, 400);
479
- if (!r.totalCommission.type || !allowedTotalCommissionTypes.includes(r.totalCommission.type)) throw new payload.APIError(`Commission rule ${index + 1}: Total Commission type must be one of ${allowedTotalCommissionTypes.join(", ")}`, 400);
480
- const type = r.totalCommission.type;
481
- const appliesTo = r.appliesTo ?? "all";
482
- if (appliesTo === "products" && (!r.products || r.products.length === 0)) throw new payload.APIError(`Commission rule ${index + 1}: At least one product is required`, 400);
483
- if ((appliesTo === "segments" || appliesTo === "categories") && (!r.categories || r.categories.length === 0) && (!r.tags || r.tags.length === 0)) throw new payload.APIError(`Commission rule ${index + 1}: At least one category or tag is required`, 400);
484
- let partnerSplit;
485
- let customerSplit;
486
- let partnerPercent = null;
487
- let customerPercent = null;
488
- let partnerAmount = null;
489
- let customerAmount = null;
490
- let splitWarning = null;
491
- if (type === "percentage") {
492
- const partnerPctInput = toNumber(r.partnerPercent) ?? toNumber(r.partnerSplit);
493
- const customerPctInput = toNumber(r.customerPercent) ?? toNumber(r.customerSplit);
494
- if (partnerPctInput == null || partnerPctInput < 0 || partnerPctInput > 100) throw new payload.APIError(`Commission rule ${index + 1}: Partner Split must be between 0 and 100`, 400);
495
- if (customerPctInput != null && (customerPctInput < 0 || customerPctInput > 100)) throw new payload.APIError(`Commission rule ${index + 1}: Customer percentage must be between 0 and 100`, 400);
496
- const customerPctComputed = customerPctInput != null ? customerPctInput : 100 - partnerPctInput;
497
- const percentTotal = partnerPctInput + customerPctComputed;
498
- if (percentTotal > 100) throw new payload.APIError(`Commission rule ${index + 1}: Partner percentage + Customer percentage cannot exceed 100`, 400);
499
- if (percentTotal > 50) splitWarning = `High total split configured: ${percentTotal}% (partner + customer).`;
500
- partnerPercent = partnerPctInput;
501
- customerPercent = customerPctComputed;
502
- partnerSplit = partnerPctInput;
503
- customerSplit = customerPctComputed;
504
- } else {
505
- const partnerAmountInput = normalizeLegacyX100Money(toNumber(r.partnerAmount));
506
- const customerAmountInput = normalizeLegacyX100Money(toNumber(r.customerAmount));
507
- const legacyPartnerSplitInput = toNumber(r.partnerSplit);
508
- const legacyCustomerSplitInput = toNumber(r.customerSplit);
509
- const hasNewFixedInputs = partnerAmountInput != null || customerAmountInput != null;
510
- const hasLegacyFixedInputs = legacyPartnerSplitInput != null || legacyCustomerSplitInput != null;
511
- if (hasNewFixedInputs) {
512
- if (partnerAmountInput == null || partnerAmountInput < 0) throw new payload.APIError(`Commission rule ${index + 1}: Partner fixed amount must be a non-negative number`, 400);
513
- if (customerAmountInput == null || customerAmountInput < 0) throw new payload.APIError(`Commission rule ${index + 1}: Customer fixed amount must be a non-negative number`, 400);
514
- partnerAmount = partnerAmountInput;
515
- customerAmount = customerAmountInput;
516
- partnerSplit = partnerAmountInput;
517
- customerSplit = customerAmountInput;
518
- } else if (hasLegacyFixedInputs) {
519
- if (legacyPartnerSplitInput == null || legacyPartnerSplitInput < 0) throw new payload.APIError(`Commission rule ${index + 1}: For fixed commissions, both partner and customer values must be non-negative numbers`, 400);
520
- const resolvedLegacyCustomerSplit = legacyCustomerSplitInput ?? 100 - legacyPartnerSplitInput;
521
- if (resolvedLegacyCustomerSplit == null || resolvedLegacyCustomerSplit < 0) throw new payload.APIError(`Commission rule ${index + 1}: For fixed commissions, both partner and customer values must be non-negative numbers`, 400);
522
- partnerSplit = legacyPartnerSplitInput;
523
- customerSplit = resolvedLegacyCustomerSplit;
524
- partnerAmount = null;
525
- customerAmount = null;
526
- } else throw new payload.APIError(`Commission rule ${index + 1}: For fixed commissions, both partner and customer values must be provided`, 400);
527
- }
528
- return {
529
- ...rule,
530
- appliesTo: appliesTo === "categories" ? "segments" : appliesTo,
531
- totalCommission: {
532
- type,
533
- ...typeof r.totalCommission.value === "number" ? { value: r.totalCommission.value } : {},
534
- ...typeof r.totalCommission.maxAmount === "number" ? { maxAmount: r.totalCommission.maxAmount } : {}
535
- },
536
- partnerPercent,
537
- customerPercent,
538
- partnerAmount,
539
- customerAmount,
540
- partnerSplit,
541
- customerSplit,
542
- splitWarning
463
+ hooks: {
464
+ beforeChange: [({ data }) => {
465
+ if (!data.commissionRules || !Array.isArray(data.commissionRules) || data.commissionRules.length === 0) throw new payload.APIError("At least one commission rule is required", 400);
466
+ const rawMaxPartner = toNumber(data.maxPartnerCommissionPerOrder);
467
+ if (rawMaxPartner != null && rawMaxPartner < 0) throw new payload.APIError("Maximum commission per order for partner must be a non-negative number", 400);
468
+ const rawMaxCustomer = toNumber(data.maxCustomerDiscountPerOrder);
469
+ if (rawMaxCustomer != null && rawMaxCustomer < 0) throw new payload.APIError("Maximum discount for customer per order must be a non-negative number", 400);
470
+ const rawMinOrder = toNumber(data.minOrderAmount);
471
+ if (rawMinOrder != null && rawMinOrder < 0) throw new payload.APIError("Minimum Order Amount must be a non-negative number", 400);
472
+ data.maxPartnerCommissionPerOrder = rawMaxPartner != null ? Math.round(rawMaxPartner * 100) : null;
473
+ data.maxCustomerDiscountPerOrder = rawMaxCustomer != null ? Math.round(rawMaxCustomer * 100) : null;
474
+ data.minOrderAmount = rawMinOrder != null ? Math.round(rawMinOrder * 100) : null;
475
+ data.commissionRules = data.commissionRules.map((rule, index) => {
476
+ const r = rule;
477
+ if (!r.totalCommission) throw new payload.APIError(`Commission rule ${index + 1}: Total Commission is required`, 400);
478
+ if (!r.totalCommission.type || !allowedTotalCommissionTypes.includes(r.totalCommission.type)) throw new payload.APIError(`Commission rule ${index + 1}: Total Commission type must be one of ${allowedTotalCommissionTypes.join(", ")}`, 400);
479
+ const type = r.totalCommission.type;
480
+ const appliesTo = r.appliesTo ?? "all";
481
+ if (appliesTo === "products" && (!r.products || r.products.length === 0)) throw new payload.APIError(`Commission rule ${index + 1}: At least one product is required`, 400);
482
+ if ((appliesTo === "segments" || appliesTo === "categories") && (!r.categories || r.categories.length === 0) && (!r.tags || r.tags.length === 0)) throw new payload.APIError(`Commission rule ${index + 1}: At least one category or tag is required`, 400);
483
+ let partnerSplit;
484
+ let customerSplit;
485
+ let partnerPercent = null;
486
+ let customerPercent = null;
487
+ let partnerAmount = null;
488
+ let customerAmount = null;
489
+ let splitWarning = null;
490
+ if (type === "percentage") {
491
+ const partnerPctInput = toNumber(r.partnerPercent) ?? toNumber(r.partnerSplit);
492
+ const customerPctInput = toNumber(r.customerPercent) ?? toNumber(r.customerSplit);
493
+ if (partnerPctInput == null || partnerPctInput < 0 || partnerPctInput > 100) throw new payload.APIError(`Commission rule ${index + 1}: Partner Split must be between 0 and 100`, 400);
494
+ if (customerPctInput != null && (customerPctInput < 0 || customerPctInput > 100)) throw new payload.APIError(`Commission rule ${index + 1}: Customer percentage must be between 0 and 100`, 400);
495
+ const customerPctComputed = customerPctInput != null ? customerPctInput : 100 - partnerPctInput;
496
+ const percentTotal = partnerPctInput + customerPctComputed;
497
+ if (percentTotal > 100) throw new payload.APIError(`Commission rule ${index + 1}: Partner percentage + Customer percentage cannot exceed 100`, 400);
498
+ if (percentTotal > 50) splitWarning = `High total split configured: ${percentTotal}% (partner + customer).`;
499
+ partnerPercent = partnerPctInput;
500
+ customerPercent = customerPctComputed;
501
+ partnerSplit = partnerPctInput;
502
+ customerSplit = customerPctComputed;
503
+ } else {
504
+ const partnerAmountInput = toNumber(r.partnerAmount);
505
+ const customerAmountInput = toNumber(r.customerAmount);
506
+ const legacyPartnerSplitInput = toNumber(r.partnerSplit);
507
+ const legacyCustomerSplitInput = toNumber(r.customerSplit);
508
+ const hasNewFixedInputs = partnerAmountInput != null || customerAmountInput != null;
509
+ const hasLegacyFixedInputs = legacyPartnerSplitInput != null || legacyCustomerSplitInput != null;
510
+ if (hasNewFixedInputs) {
511
+ if (partnerAmountInput == null || partnerAmountInput < 0) throw new payload.APIError(`Commission rule ${index + 1}: Partner fixed amount must be a non-negative number`, 400);
512
+ if (customerAmountInput == null || customerAmountInput < 0) throw new payload.APIError(`Commission rule ${index + 1}: Customer fixed amount must be a non-negative number`, 400);
513
+ partnerAmount = partnerAmountInput;
514
+ customerAmount = customerAmountInput;
515
+ partnerSplit = partnerAmountInput;
516
+ customerSplit = customerAmountInput;
517
+ } else if (hasLegacyFixedInputs) {
518
+ if (legacyPartnerSplitInput == null || legacyPartnerSplitInput < 0) throw new payload.APIError(`Commission rule ${index + 1}: For fixed commissions, both partner and customer values must be non-negative numbers`, 400);
519
+ const resolvedLegacyCustomerSplit = legacyCustomerSplitInput ?? 100 - legacyPartnerSplitInput;
520
+ if (resolvedLegacyCustomerSplit == null || resolvedLegacyCustomerSplit < 0) throw new payload.APIError(`Commission rule ${index + 1}: For fixed commissions, both partner and customer values must be non-negative numbers`, 400);
521
+ partnerSplit = legacyPartnerSplitInput;
522
+ customerSplit = resolvedLegacyCustomerSplit;
523
+ partnerAmount = null;
524
+ customerAmount = null;
525
+ } else throw new payload.APIError(`Commission rule ${index + 1}: For fixed commissions, both partner and customer values must be provided`, 400);
526
+ }
527
+ return {
528
+ ...rule,
529
+ appliesTo: appliesTo === "categories" ? "segments" : appliesTo,
530
+ totalCommission: {
531
+ type,
532
+ ...typeof r.totalCommission.value === "number" ? { value: r.totalCommission.value } : {},
533
+ ...typeof r.totalCommission.maxAmount === "number" ? { maxAmount: r.totalCommission.maxAmount } : {}
534
+ },
535
+ partnerPercent,
536
+ customerPercent,
537
+ partnerAmount,
538
+ customerAmount,
539
+ partnerSplit,
540
+ customerSplit,
541
+ splitWarning
542
+ };
543
+ });
544
+ return data;
545
+ }],
546
+ afterRead: [({ doc }) => {
547
+ if (!doc) return doc;
548
+ const unscale = (value) => {
549
+ const n = toNumber(value);
550
+ if (n == null) return null;
551
+ return Math.round(n / 100 * 100) / 100;
543
552
  };
544
- });
545
- return data;
546
- }] },
553
+ doc.maxPartnerCommissionPerOrder = unscale(doc.maxPartnerCommissionPerOrder);
554
+ doc.maxCustomerDiscountPerOrder = unscale(doc.maxCustomerDiscountPerOrder);
555
+ doc.minOrderAmount = unscale(doc.minOrderAmount);
556
+ return doc;
557
+ }]
558
+ },
547
559
  fields: [
548
560
  {
549
561
  name: "name",
@@ -561,19 +573,19 @@ const createReferralProgramsCollection = (pluginConfig) => {
561
573
  name: "maxPartnerCommissionPerOrder",
562
574
  type: "number",
563
575
  min: 0,
564
- admin: { description: "Maximum commission per order for partner (normal currency value, ). Leave empty for no cap." }
576
+ admin: { description: "Maximum commission per order for partner (enter normal currency value, e.g. 50 for $50). Leave empty for no cap." }
565
577
  },
566
578
  {
567
579
  name: "maxCustomerDiscountPerOrder",
568
580
  type: "number",
569
581
  min: 0,
570
- admin: { description: "Maximum customer discount per order (normal currency value, ). Leave empty for no cap." }
582
+ admin: { description: "Maximum customer discount per order (enter normal currency value, e.g. 25 for $25). Leave empty for no cap." }
571
583
  },
572
584
  {
573
585
  name: "minOrderAmount",
574
586
  type: "number",
575
587
  min: 0,
576
- admin: { description: "Minimum cart subtotal required for this program (normal currency value, ). Leave empty for no minimum." }
588
+ admin: { description: "Minimum cart subtotal required for this program (enter normal currency value, e.g. 100 for $100). Leave empty for no minimum." }
577
589
  },
578
590
  {
579
591
  name: "commissionRules",
@@ -654,7 +666,7 @@ const createReferralProgramsCollection = (pluginConfig) => {
654
666
  max: 100,
655
667
  admin: {
656
668
  condition: (_, siblingData) => siblingData?.totalCommission?.type === "percentage",
657
- description: "Partner share in percent (0-100)"
669
+ description: "Partner share in percent (0100)"
658
670
  }
659
671
  },
660
672
  {
@@ -664,7 +676,7 @@ const createReferralProgramsCollection = (pluginConfig) => {
664
676
  max: 100,
665
677
  admin: {
666
678
  condition: (_, siblingData) => siblingData?.totalCommission?.type === "percentage",
667
- description: "Customer share percentage. (0-100). Partner + Customer cannot exceed 100."
679
+ description: "Customer share percentage (0100). Partner + Customer cannot exceed 100."
668
680
  }
669
681
  },
670
682
  {
@@ -673,7 +685,7 @@ const createReferralProgramsCollection = (pluginConfig) => {
673
685
  min: 0,
674
686
  admin: {
675
687
  condition: (_, siblingData) => siblingData?.totalCommission?.type === "fixed",
676
- description: "Fixed partner commission amount per item (normal currency value)."
688
+ description: "Fixed partner commission amount per item (normal currency value, e.g. 12.50 for $12.50)."
677
689
  }
678
690
  },
679
691
  {
@@ -682,7 +694,7 @@ const createReferralProgramsCollection = (pluginConfig) => {
682
694
  min: 0,
683
695
  admin: {
684
696
  condition: (_, siblingData) => siblingData?.totalCommission?.type === "fixed",
685
- description: "Fixed customer discount amount per item (normal currency value)."
697
+ description: "Fixed customer discount amount per item (normal currency value, e.g. 5 for $5)."
686
698
  }
687
699
  },
688
700
  {
@@ -691,7 +703,7 @@ const createReferralProgramsCollection = (pluginConfig) => {
691
703
  min: 0,
692
704
  admin: {
693
705
  hidden: true,
694
- description: "Canonical storage field. Percentage mode: percent. Fixed mode: amount."
706
+ description: "Canonical storage field. Percentage mode: percent value. Fixed mode: per-item amount."
695
707
  }
696
708
  },
697
709
  {
@@ -700,7 +712,7 @@ const createReferralProgramsCollection = (pluginConfig) => {
700
712
  min: 0,
701
713
  admin: {
702
714
  hidden: true,
703
- description: "Canonical storage field. Percentage mode: percent. Fixed mode: amount."
715
+ description: "Canonical storage field. Percentage mode: percent value. Fixed mode: per-item amount."
704
716
  }
705
717
  }
706
718
  ]
@@ -744,16 +756,35 @@ function getCartItemUnitPrice({ item, product, variant, currencyCode, defaultCur
744
756
  }
745
757
  //#endregion
746
758
  //#region src/utilities/calculateValues.ts
759
+ /** Convert a normal-currency amount to integer cents. */
760
+ function toCents(amount) {
761
+ return Math.round(amount * 100);
762
+ }
763
+ /** Convert integer cents back to a normal-currency amount (2 dp max). */
764
+ function fromCents(cents) {
765
+ return Math.round(cents) / 100;
766
+ }
767
+ /**
768
+ * Calculate the discount amount for a coupon.
769
+ *
770
+ * @param coupon - Coupon document from DB (values in normal currency).
771
+ * @param cartTotal - Cart subtotal in normal currency.
772
+ * @returns Discount amount in normal currency (2 dp).
773
+ */
747
774
  function calculateCouponDiscount({ coupon, cartTotal }) {
748
- let discount = 0;
775
+ const cartCents = toCents(cartTotal);
776
+ let discountCents = 0;
749
777
  if (coupon.type === "percentage") {
750
- discount = roundTo2(cartTotal * coupon.value / 100);
751
- if (coupon.maxDiscountAmount != null && discount > coupon.maxDiscountAmount) discount = roundTo2(coupon.maxDiscountAmount);
778
+ discountCents = Math.floor(cartCents * coupon.value / 100);
779
+ if (coupon.maxDiscountAmount != null) {
780
+ const maxCents = toCents(coupon.maxDiscountAmount);
781
+ if (discountCents > maxCents) discountCents = maxCents;
782
+ }
752
783
  } else if (coupon.type === "fixed") {
753
- discount = roundTo2(coupon.value);
754
- if (discount > cartTotal) discount = roundTo2(cartTotal);
784
+ discountCents = toCents(coupon.value);
785
+ if (discountCents > cartCents) discountCents = cartCents;
755
786
  }
756
- return roundTo2(discount);
787
+ return fromCents(discountCents);
757
788
  }
758
789
  function relationId$5(value) {
759
790
  if (value == null) return null;
@@ -774,19 +805,25 @@ function getRuleSplits(rule) {
774
805
  customerSplit: typeof rule.customerSplit === "number" ? rule.customerSplit : typeof rule.refereeSplit === "number" ? rule.refereeSplit : 100 - partnerRaw
775
806
  };
776
807
  }
777
- function calculateItemRewardByRule({ rule, itemTotal, quantity, allowedTotalCommissionTypes }) {
808
+ /**
809
+ * Calculate partner and customer reward for a single line item.
810
+ *
811
+ * ALL inputs are expected in CENTS.
812
+ * Returns rewards in CENTS, or null if the rule is inapplicable.
813
+ */
814
+ function calculateItemRewardByRule({ rule, itemTotalCents, quantity, allowedTotalCommissionTypes }) {
778
815
  const allowedTypes = allowedCommissionTypesSet(allowedTotalCommissionTypes);
779
816
  if (rule.totalCommission) {
780
817
  if (!allowedTypes.has(rule.totalCommission.type)) return null;
781
- const resolvedMaxAmount = typeof rule.totalCommission.maxAmount === "number" && Number.isFinite(rule.totalCommission.maxAmount) ? rule.totalCommission.maxAmount : null;
818
+ const resolvedMaxAmountCents = typeof rule.totalCommission.maxAmount === "number" && Number.isFinite(rule.totalCommission.maxAmount) ? toCents(rule.totalCommission.maxAmount) : null;
782
819
  if (rule.totalCommission.type === "fixed" && rule.totalCommission.value == null) {
783
- const partnerAmtPerUnit = typeof rule.partnerSplit === "number" ? rule.partnerSplit : null;
784
- const customerAmtPerUnit = typeof rule.customerSplit === "number" ? rule.customerSplit : null;
785
- if (partnerAmtPerUnit == null || customerAmtPerUnit == null) return null;
786
- let partner = partnerAmtPerUnit * quantity;
787
- let customer = customerAmtPerUnit * quantity;
788
- if (resolvedMaxAmount != null) {
789
- const maxPotForLine = resolvedMaxAmount * quantity;
820
+ const partnerAmtPerUnitCents = typeof rule.partnerSplit === "number" ? toCents(rule.partnerSplit) : null;
821
+ const customerAmtPerUnitCents = typeof rule.customerSplit === "number" ? toCents(rule.customerSplit) : null;
822
+ if (partnerAmtPerUnitCents == null || customerAmtPerUnitCents == null) return null;
823
+ let partner = partnerAmtPerUnitCents * quantity;
824
+ let customer = customerAmtPerUnitCents * quantity;
825
+ if (resolvedMaxAmountCents != null) {
826
+ const maxPotForLine = resolvedMaxAmountCents * quantity;
790
827
  const totalPot = partner + customer;
791
828
  if (totalPot > maxPotForLine && totalPot > 0) {
792
829
  const ratio = maxPotForLine / totalPot;
@@ -799,7 +836,6 @@ function calculateItemRewardByRule({ rule, itemTotal, quantity, allowedTotalComm
799
836
  customer
800
837
  };
801
838
  }
802
- let totalPot = 0;
803
839
  if (rule.totalCommission.type === "percentage") {
804
840
  const commissionValue = typeof rule.totalCommission.value === "number" && Number.isFinite(rule.totalCommission.value) ? rule.totalCommission.value : null;
805
841
  if (commissionValue == null) {
@@ -807,58 +843,63 @@ function calculateItemRewardByRule({ rule, itemTotal, quantity, allowedTotalComm
807
843
  if (partnerPercentInput == null || partnerPercentInput < 0 || partnerPercentInput > 100) return null;
808
844
  const customerPercentInput = typeof rule.customerPercent === "number" ? rule.customerPercent : typeof rule.customerSplit === "number" ? rule.customerSplit : 100 - partnerPercentInput;
809
845
  if (customerPercentInput == null || customerPercentInput < 0 || customerPercentInput > 100) return null;
810
- const partner = itemTotal * partnerPercentInput / 100;
811
- const customer = itemTotal * customerPercentInput / 100;
812
- if (resolvedMaxAmount != null) {
813
- const maxPotForLine = resolvedMaxAmount * quantity;
846
+ let partner = Math.floor(itemTotalCents * partnerPercentInput / 100);
847
+ let customer = Math.floor(itemTotalCents * customerPercentInput / 100);
848
+ if (resolvedMaxAmountCents != null) {
849
+ const maxPotForLine = resolvedMaxAmountCents * quantity;
814
850
  const totalForLine = partner + customer;
815
851
  if (totalForLine > maxPotForLine && totalForLine > 0) {
816
852
  const ratio = maxPotForLine / totalForLine;
817
- return {
818
- partner: Math.floor(partner * ratio),
819
- customer: Math.floor(customer * ratio)
820
- };
853
+ partner = Math.floor(partner * ratio);
854
+ customer = Math.floor(customer * ratio);
821
855
  }
822
856
  }
823
857
  return {
824
- partner: Math.floor(partner),
825
- customer: Math.floor(customer)
858
+ partner,
859
+ customer
826
860
  };
827
861
  }
828
- totalPot = itemTotal * commissionValue / 100;
829
- } else {
862
+ let totalPotCents = Math.floor(itemTotalCents * commissionValue / 100);
863
+ if (resolvedMaxAmountCents != null) {
864
+ const maxPotForLine = resolvedMaxAmountCents * quantity;
865
+ if (totalPotCents > maxPotForLine) totalPotCents = maxPotForLine;
866
+ }
830
867
  const splits = getRuleSplits(rule);
831
868
  if (!splits) return null;
832
- totalPot = rule.totalCommission.value * quantity;
833
- if (resolvedMaxAmount != null) {
834
- const maxPotForLine = resolvedMaxAmount * quantity;
835
- if (totalPot > maxPotForLine) totalPot = maxPotForLine;
836
- }
837
869
  return {
838
- partner: Math.floor(totalPot * splits.partnerSplit / 100),
839
- customer: Math.floor(totalPot * splits.customerSplit / 100)
870
+ partner: Math.floor(totalPotCents * splits.partnerSplit / 100),
871
+ customer: Math.floor(totalPotCents * splits.customerSplit / 100)
840
872
  };
841
873
  }
842
- if (resolvedMaxAmount != null) {
843
- const maxPotForLine = resolvedMaxAmount * quantity;
844
- if (totalPot > maxPotForLine) totalPot = maxPotForLine;
874
+ {
875
+ const splits = getRuleSplits(rule);
876
+ if (!splits) return null;
877
+ let totalPotCents = toCents(rule.totalCommission.value) * quantity;
878
+ if (resolvedMaxAmountCents != null) {
879
+ const maxPotForLine = resolvedMaxAmountCents * quantity;
880
+ if (totalPotCents > maxPotForLine) totalPotCents = maxPotForLine;
881
+ }
882
+ return {
883
+ partner: Math.floor(totalPotCents * splits.partnerSplit / 100),
884
+ customer: Math.floor(totalPotCents * splits.customerSplit / 100)
885
+ };
845
886
  }
846
- const splits = getRuleSplits(rule);
847
- if (!splits) return null;
848
- return {
849
- partner: Math.floor(totalPot * splits.partnerSplit / 100),
850
- customer: Math.floor(totalPot * splits.customerSplit / 100)
851
- };
852
887
  }
853
888
  if (rule.referrerReward && rule.refereeReward) {
854
889
  let partner = 0;
855
- if (rule.referrerReward.type === "percentage") partner = itemTotal * rule.referrerReward.value / 100;
856
- else partner = rule.referrerReward.value * quantity;
857
- if (rule.referrerReward.maxReward != null && partner > rule.referrerReward.maxReward) partner = rule.referrerReward.maxReward;
890
+ if (rule.referrerReward.type === "percentage") partner = Math.floor(itemTotalCents * rule.referrerReward.value / 100);
891
+ else partner = toCents(rule.referrerReward.value) * quantity;
892
+ if (rule.referrerReward.maxReward != null) {
893
+ const maxCents = toCents(rule.referrerReward.maxReward);
894
+ if (partner > maxCents) partner = maxCents;
895
+ }
858
896
  let customer = 0;
859
- if (rule.refereeReward.type === "percentage") customer = itemTotal * rule.refereeReward.value / 100;
860
- else customer = rule.refereeReward.value * quantity;
861
- if (rule.refereeReward.maxReward != null && customer > rule.refereeReward.maxReward) customer = rule.refereeReward.maxReward;
897
+ if (rule.refereeReward.type === "percentage") customer = Math.floor(itemTotalCents * rule.refereeReward.value / 100);
898
+ else customer = toCents(rule.refereeReward.value) * quantity;
899
+ if (rule.refereeReward.maxReward != null) {
900
+ const maxCents = toCents(rule.refereeReward.maxReward);
901
+ if (customer > maxCents) customer = maxCents;
902
+ }
862
903
  return {
863
904
  partner,
864
905
  customer
@@ -874,12 +915,12 @@ function getItemCategoryIds(item) {
874
915
  function getItemTagIds(item) {
875
916
  return Array.isArray(item?.product?.tags) ? normalizeIds(item.product.tags) : [];
876
917
  }
877
- function selectBestRuleForItem({ rules, item, itemTotal, quantity, cartTotal, minOrderAmount, allowedTotalCommissionTypes }) {
918
+ function selectBestRuleForItem({ rules, item, itemTotalCents, quantity, cartTotalCents, minOrderAmountCents, allowedTotalCommissionTypes }) {
878
919
  const allowedTypes = allowedCommissionTypesSet(allowedTotalCommissionTypes);
879
920
  const eligibleRules = rules.filter((rule) => {
880
921
  if (!(rule?.totalCommission?.type ? allowedTypes.has(rule.totalCommission.type) : true)) return false;
881
- const resolvedMinOrderAmount = typeof minOrderAmount === "number" && Number.isFinite(minOrderAmount) ? minOrderAmount : typeof rule?.minOrderAmount === "number" && Number.isFinite(rule.minOrderAmount) ? rule.minOrderAmount : null;
882
- if (resolvedMinOrderAmount != null) return cartTotal >= resolvedMinOrderAmount;
922
+ const resolvedMinCents = minOrderAmountCents != null && Number.isFinite(minOrderAmountCents) ? minOrderAmountCents : typeof rule?.minOrderAmount === "number" && Number.isFinite(rule.minOrderAmount) ? toCents(rule.minOrderAmount) : null;
923
+ if (resolvedMinCents != null) return cartTotalCents >= resolvedMinCents;
883
924
  return true;
884
925
  });
885
926
  const productId = relationId$5(item.product);
@@ -902,7 +943,7 @@ function selectBestRuleForItem({ rules, item, itemTotal, quantity, cartTotal, mi
902
943
  for (const rule of candidates) {
903
944
  const reward = calculateItemRewardByRule({
904
945
  rule,
905
- itemTotal,
946
+ itemTotalCents,
906
947
  quantity,
907
948
  allowedTotalCommissionTypes
908
949
  });
@@ -928,6 +969,10 @@ function selectBestRuleForItem({ rules, item, itemTotal, quantity, cartTotal, mi
928
969
  }
929
970
  return best;
930
971
  }
972
+ /**
973
+ * Returns the effective minimum order amount for a program in NORMAL CURRENCY.
974
+ * Returns null if there is no minimum.
975
+ */
931
976
  function getProgramMinimumOrderAmount({ program, allowedTotalCommissionTypes }) {
932
977
  if (typeof program?.minOrderAmount === "number" && Number.isFinite(program.minOrderAmount)) return program.minOrderAmount;
933
978
  const rules = Array.isArray(program?.commissionRules) ? program.commissionRules : [];
@@ -940,50 +985,66 @@ function getProgramMinimumOrderAmount({ program, allowedTotalCommissionTypes })
940
985
  if (!minValues.length) return null;
941
986
  return Math.min(...minValues);
942
987
  }
988
+ /**
989
+ * Calculate total partner commission and customer discount for a cart.
990
+ *
991
+ * All monetary inputs are in NORMAL CURRENCY.
992
+ * Returns results in NORMAL CURRENCY (2 dp).
993
+ */
943
994
  function calculateCommissionAndDiscount({ cartItems, program, currencyCode = "AED", cartTotal = 0, allowedTotalCommissionTypes }) {
944
995
  const rules = Array.isArray(program?.commissionRules) ? program.commissionRules : [];
945
996
  if (!rules.length) return {
946
997
  partnerCommission: 0,
947
998
  customerDiscount: 0
948
999
  };
949
- let totalPartnerCommission = 0;
950
- let totalCustomerDiscount = 0;
1000
+ const cartTotalCents = toCents(cartTotal);
1001
+ const programMinOrderAmountCents = typeof program?.minOrderAmount === "number" && Number.isFinite(program.minOrderAmount) ? toCents(program.minOrderAmount) : null;
1002
+ let totalPartnerCents = 0;
1003
+ let totalCustomerCents = 0;
951
1004
  for (const item of cartItems) {
952
1005
  const product = typeof item.product === "object" ? item.product : {};
953
- const itemPrice = getCartItemUnitPrice({
1006
+ const itemPriceCurrency = getCartItemUnitPrice({
954
1007
  item,
955
1008
  product,
956
1009
  variant: typeof item.variant === "object" ? item.variant : {},
957
1010
  currencyCode
958
1011
  });
959
1012
  const quantity = item.quantity ?? 1;
960
- const itemTotal = itemPrice * quantity;
1013
+ const itemTotalCents = toCents(itemPriceCurrency) * quantity;
961
1014
  const bestMatch = selectBestRuleForItem({
962
1015
  rules,
963
1016
  item: {
964
1017
  ...item,
965
1018
  product
966
1019
  },
967
- itemTotal,
1020
+ itemTotalCents,
968
1021
  quantity,
969
- cartTotal,
970
- minOrderAmount: typeof program?.minOrderAmount === "number" && Number.isFinite(program.minOrderAmount) ? program.minOrderAmount : null,
1022
+ cartTotalCents,
1023
+ minOrderAmountCents: programMinOrderAmountCents,
971
1024
  allowedTotalCommissionTypes
972
1025
  });
973
1026
  if (!bestMatch) continue;
974
- totalPartnerCommission += bestMatch.reward.partner;
975
- totalCustomerDiscount += bestMatch.reward.customer;
1027
+ totalPartnerCents += bestMatch.reward.partner;
1028
+ totalCustomerCents += bestMatch.reward.customer;
976
1029
  }
977
- const maxPartnerCommissionPerOrder = typeof program?.maxPartnerCommissionPerOrder === "number" && Number.isFinite(program.maxPartnerCommissionPerOrder) ? program.maxPartnerCommissionPerOrder : null;
978
- const maxCustomerDiscountPerOrder = typeof program?.maxCustomerDiscountPerOrder === "number" && Number.isFinite(program.maxCustomerDiscountPerOrder) ? program.maxCustomerDiscountPerOrder : null;
979
- if (maxPartnerCommissionPerOrder != null) totalPartnerCommission = Math.min(totalPartnerCommission, maxPartnerCommissionPerOrder);
980
- if (maxCustomerDiscountPerOrder != null) totalCustomerDiscount = Math.min(totalCustomerDiscount, maxCustomerDiscountPerOrder);
1030
+ const maxPartnerCents = typeof program?.maxPartnerCommissionPerOrder === "number" && Number.isFinite(program.maxPartnerCommissionPerOrder) ? toCents(program.maxPartnerCommissionPerOrder) : null;
1031
+ const maxCustomerCents = typeof program?.maxCustomerDiscountPerOrder === "number" && Number.isFinite(program.maxCustomerDiscountPerOrder) ? toCents(program.maxCustomerDiscountPerOrder) : null;
1032
+ if (maxPartnerCents != null) totalPartnerCents = Math.min(totalPartnerCents, maxPartnerCents);
1033
+ if (maxCustomerCents != null) totalCustomerCents = Math.min(totalCustomerCents, maxCustomerCents);
981
1034
  return {
982
- partnerCommission: totalPartnerCommission,
983
- customerDiscount: totalCustomerDiscount
1035
+ partnerCommission: fromCents(totalPartnerCents),
1036
+ customerDiscount: fromCents(totalCustomerCents)
984
1037
  };
985
1038
  }
986
1039
  //#endregion
1040
+ //#region src/utilities/roundTo2.ts
1041
+ /**
1042
+ * Rounds a number to 2 decimal places (standard for monetary values).
1043
+ */
1044
+ function roundTo2(value) {
1045
+ return Math.round(value * 100) / 100;
1046
+ }
1047
+ //#endregion
987
1048
  //#region src/endpoints/applyCoupon.ts
988
1049
  function relationId$4(value) {
989
1050
  if (value == null) return null;
@@ -1176,17 +1237,17 @@ async function handleCouponCode({ payload: payload$7, cart, cartID, normalizedCo
1176
1237
  }, { status: 400 });
1177
1238
  const cartSubtotal = Number(resolvers.getCartSubtotal(cart)) || 0;
1178
1239
  const cartTotal = Number(resolvers.getCartTotal(cart)) || cartSubtotal || 0;
1179
- if (coupon.minOrderValue && cartTotal < coupon.minOrderValue) return Response.json({
1240
+ if (coupon.minOrderValue && cartSubtotal < coupon.minOrderValue) return Response.json({
1180
1241
  success: false,
1181
1242
  error: `Minimum order value of ${coupon.minOrderValue} ${pluginConfig.defaultCurrency} required`
1182
1243
  }, { status: 400 });
1183
- if (coupon.maxOrderValue && cartTotal > coupon.maxOrderValue) return Response.json({
1244
+ if (coupon.maxOrderValue && cartSubtotal > coupon.maxOrderValue) return Response.json({
1184
1245
  success: false,
1185
1246
  error: `Maximum order value of ${coupon.maxOrderValue} ${pluginConfig.defaultCurrency} exceeded`
1186
1247
  }, { status: 400 });
1187
1248
  const discountAmount = calculateCouponDiscount({
1188
1249
  coupon,
1189
- cartTotal
1250
+ cartTotal: cartSubtotal
1190
1251
  });
1191
1252
  const nextTotal = roundTo2(Math.max(0, cartTotal - discountAmount));
1192
1253
  const data = {};
@@ -1248,12 +1309,13 @@ async function handleReferralCode({ payload: payload$8, cart, cartID, normalized
1248
1309
  error: "Referral code already applied to this cart"
1249
1310
  }, { status: 400 });
1250
1311
  const cartItems = resolvers.getCartItems(cart);
1251
- const cartTotal = Number(resolvers.getCartTotal(cart)) || Number(resolvers.getCartSubtotal(cart)) || 0;
1312
+ const cartSubtotal = Number(resolvers.getCartSubtotal(cart)) || 0;
1313
+ const cartTotal = Number(resolvers.getCartTotal(cart)) || cartSubtotal || 0;
1252
1314
  const minOrderAmount = getProgramMinimumOrderAmount({
1253
1315
  program,
1254
1316
  allowedTotalCommissionTypes: pluginConfig.referralConfig.allowedTotalCommissionTypes
1255
1317
  });
1256
- if (typeof minOrderAmount === "number" && cartTotal < minOrderAmount) return Response.json({
1318
+ if (typeof minOrderAmount === "number" && cartSubtotal < minOrderAmount) return Response.json({
1257
1319
  success: false,
1258
1320
  error: `Minimum order value of ${minOrderAmount} ${pluginConfig.defaultCurrency} required for this referral program`
1259
1321
  }, { status: 400 });
@@ -1261,7 +1323,7 @@ async function handleReferralCode({ payload: payload$8, cart, cartID, normalized
1261
1323
  cartItems,
1262
1324
  program,
1263
1325
  currencyCode: pluginConfig.defaultCurrency,
1264
- cartTotal,
1326
+ cartTotal: cartSubtotal,
1265
1327
  allowedTotalCommissionTypes: pluginConfig.referralConfig.allowedTotalCommissionTypes
1266
1328
  });
1267
1329
  const roundedPartnerCommission = roundTo2(partnerCommission);
@@ -1690,7 +1752,7 @@ async function validateReferralCode({ payload: payload$4, normalizedCode, cartID
1690
1752
  id: cartID,
1691
1753
  depth: 2
1692
1754
  }) : null;
1693
- const cartTotal = cart ? Number(resolvers.getCartTotal(cart)) || Number(resolvers.getCartSubtotal(cart)) || 0 : 0;
1755
+ const cartTotal = cart ? Number(resolvers.getCartSubtotal(cart)) || Number(resolvers.getCartTotal(cart)) || 0 : 0;
1694
1756
  const minOrderAmount = getProgramMinimumOrderAmount({
1695
1757
  program,
1696
1758
  allowedTotalCommissionTypes: pluginConfig.referralConfig.allowedTotalCommissionTypes