@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/collections/createReferralProgramsCollection.d.ts +11 -0
- package/dist/collections/createReferralProgramsCollection.d.ts.map +1 -1
- package/dist/endpoints/applyCoupon.d.ts.map +1 -1
- package/dist/endpoints/validateCoupon.d.ts.map +1 -1
- package/dist/index.js +241 -179
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +241 -179
- package/dist/index.mjs.map +1 -1
- package/dist/utilities/calculateValues.d.ts +18 -1
- package/dist/utilities/calculateValues.d.ts.map +1 -1
- package/package.json +1 -1
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
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
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: {
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
if (
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
if (
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
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
|
-
|
|
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
|
|
669
|
+
description: "Partner share in percent (0–100)"
|
|
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
|
|
679
|
+
description: "Customer share percentage (0–100). 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
|
-
|
|
775
|
+
const cartCents = toCents(cartTotal);
|
|
776
|
+
let discountCents = 0;
|
|
749
777
|
if (coupon.type === "percentage") {
|
|
750
|
-
|
|
751
|
-
if (coupon.maxDiscountAmount != null
|
|
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
|
-
|
|
754
|
-
if (
|
|
784
|
+
discountCents = toCents(coupon.value);
|
|
785
|
+
if (discountCents > cartCents) discountCents = cartCents;
|
|
755
786
|
}
|
|
756
|
-
return
|
|
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
|
-
|
|
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
|
|
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
|
|
784
|
-
const
|
|
785
|
-
if (
|
|
786
|
-
let partner =
|
|
787
|
-
let customer =
|
|
788
|
-
if (
|
|
789
|
-
const maxPotForLine =
|
|
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
|
-
|
|
811
|
-
|
|
812
|
-
if (
|
|
813
|
-
const maxPotForLine =
|
|
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
|
-
|
|
818
|
-
|
|
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
|
|
825
|
-
customer
|
|
858
|
+
partner,
|
|
859
|
+
customer
|
|
826
860
|
};
|
|
827
861
|
}
|
|
828
|
-
|
|
829
|
-
|
|
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(
|
|
839
|
-
customer: Math.floor(
|
|
870
|
+
partner: Math.floor(totalPotCents * splits.partnerSplit / 100),
|
|
871
|
+
customer: Math.floor(totalPotCents * splits.customerSplit / 100)
|
|
840
872
|
};
|
|
841
873
|
}
|
|
842
|
-
|
|
843
|
-
const
|
|
844
|
-
if (
|
|
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 =
|
|
856
|
-
else partner = rule.referrerReward.value * quantity;
|
|
857
|
-
if (rule.referrerReward.maxReward != null
|
|
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 =
|
|
860
|
-
else customer = rule.refereeReward.value * quantity;
|
|
861
|
-
if (rule.refereeReward.maxReward != null
|
|
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,
|
|
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
|
|
882
|
-
if (
|
|
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
|
-
|
|
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
|
-
|
|
950
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
1020
|
+
itemTotalCents,
|
|
968
1021
|
quantity,
|
|
969
|
-
|
|
970
|
-
|
|
1022
|
+
cartTotalCents,
|
|
1023
|
+
minOrderAmountCents: programMinOrderAmountCents,
|
|
971
1024
|
allowedTotalCommissionTypes
|
|
972
1025
|
});
|
|
973
1026
|
if (!bestMatch) continue;
|
|
974
|
-
|
|
975
|
-
|
|
1027
|
+
totalPartnerCents += bestMatch.reward.partner;
|
|
1028
|
+
totalCustomerCents += bestMatch.reward.customer;
|
|
976
1029
|
}
|
|
977
|
-
const
|
|
978
|
-
const
|
|
979
|
-
if (
|
|
980
|
-
if (
|
|
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:
|
|
983
|
-
customerDiscount:
|
|
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 &&
|
|
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 &&
|
|
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
|
|
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" &&
|
|
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.
|
|
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
|