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