@wtree/payload-ecommerce-coupon 3.78.6 → 3.78.8
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 +125 -103
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +125 -103
- package/dist/index.mjs.map +1 -1
- package/dist/utilities/calculateValues.d.ts.map +1 -1
- package/package.json +1 -1
package/dist/index.mjs
CHANGED
|
@@ -426,6 +426,17 @@ const createReferralCodesCollection = (pluginConfig) => {
|
|
|
426
426
|
function toNumber(value) {
|
|
427
427
|
return typeof value === "number" && Number.isFinite(value) ? value : null;
|
|
428
428
|
}
|
|
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
|
+
*/
|
|
429
440
|
const createReferralProgramsCollection = (pluginConfig) => {
|
|
430
441
|
const { collections, access, adminGroups, referralConfig, integration } = pluginConfig;
|
|
431
442
|
const allowedTotalCommissionTypes = referralConfig.allowedTotalCommissionTypes;
|
|
@@ -448,88 +459,102 @@ const createReferralProgramsCollection = (pluginConfig) => {
|
|
|
448
459
|
update: access.isAdmin || (() => false),
|
|
449
460
|
delete: access.isAdmin || (() => false)
|
|
450
461
|
},
|
|
451
|
-
hooks: {
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
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
|
-
if (
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
if (
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
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;
|
|
529
551
|
};
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
552
|
+
doc.maxPartnerCommissionPerOrder = unscale(doc.maxPartnerCommissionPerOrder);
|
|
553
|
+
doc.maxCustomerDiscountPerOrder = unscale(doc.maxCustomerDiscountPerOrder);
|
|
554
|
+
doc.minOrderAmount = unscale(doc.minOrderAmount);
|
|
555
|
+
return doc;
|
|
556
|
+
}]
|
|
557
|
+
},
|
|
533
558
|
fields: [
|
|
534
559
|
{
|
|
535
560
|
name: "name",
|
|
@@ -547,19 +572,19 @@ const createReferralProgramsCollection = (pluginConfig) => {
|
|
|
547
572
|
name: "maxPartnerCommissionPerOrder",
|
|
548
573
|
type: "number",
|
|
549
574
|
min: 0,
|
|
550
|
-
admin: { description: "Maximum commission per order for partner. 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." }
|
|
551
576
|
},
|
|
552
577
|
{
|
|
553
578
|
name: "maxCustomerDiscountPerOrder",
|
|
554
579
|
type: "number",
|
|
555
580
|
min: 0,
|
|
556
|
-
admin: { description: "Maximum customer discount per order. 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." }
|
|
557
582
|
},
|
|
558
583
|
{
|
|
559
584
|
name: "minOrderAmount",
|
|
560
585
|
type: "number",
|
|
561
586
|
min: 0,
|
|
562
|
-
admin: { description: "Minimum cart subtotal required for this program. 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." }
|
|
563
588
|
},
|
|
564
589
|
{
|
|
565
590
|
name: "commissionRules",
|
|
@@ -640,7 +665,7 @@ const createReferralProgramsCollection = (pluginConfig) => {
|
|
|
640
665
|
max: 100,
|
|
641
666
|
admin: {
|
|
642
667
|
condition: (_, siblingData) => siblingData?.totalCommission?.type === "percentage",
|
|
643
|
-
description: "Partner share in percent (0
|
|
668
|
+
description: "Partner share in percent (0–100)"
|
|
644
669
|
}
|
|
645
670
|
},
|
|
646
671
|
{
|
|
@@ -650,7 +675,7 @@ const createReferralProgramsCollection = (pluginConfig) => {
|
|
|
650
675
|
max: 100,
|
|
651
676
|
admin: {
|
|
652
677
|
condition: (_, siblingData) => siblingData?.totalCommission?.type === "percentage",
|
|
653
|
-
description: "Customer share percentage
|
|
678
|
+
description: "Customer share percentage (0–100). Partner + Customer cannot exceed 100."
|
|
654
679
|
}
|
|
655
680
|
},
|
|
656
681
|
{
|
|
@@ -659,7 +684,7 @@ const createReferralProgramsCollection = (pluginConfig) => {
|
|
|
659
684
|
min: 0,
|
|
660
685
|
admin: {
|
|
661
686
|
condition: (_, siblingData) => siblingData?.totalCommission?.type === "fixed",
|
|
662
|
-
description: "Fixed partner commission amount per item."
|
|
687
|
+
description: "Fixed partner commission amount per item (normal currency value, e.g. 12.50 for $12.50)."
|
|
663
688
|
}
|
|
664
689
|
},
|
|
665
690
|
{
|
|
@@ -668,7 +693,7 @@ const createReferralProgramsCollection = (pluginConfig) => {
|
|
|
668
693
|
min: 0,
|
|
669
694
|
admin: {
|
|
670
695
|
condition: (_, siblingData) => siblingData?.totalCommission?.type === "fixed",
|
|
671
|
-
description: "Fixed customer discount amount per item."
|
|
696
|
+
description: "Fixed customer discount amount per item (normal currency value, e.g. 5 for $5)."
|
|
672
697
|
}
|
|
673
698
|
},
|
|
674
699
|
{
|
|
@@ -677,7 +702,7 @@ const createReferralProgramsCollection = (pluginConfig) => {
|
|
|
677
702
|
min: 0,
|
|
678
703
|
admin: {
|
|
679
704
|
hidden: true,
|
|
680
|
-
description: "Canonical storage field. Percentage mode: percent. Fixed mode: amount."
|
|
705
|
+
description: "Canonical storage field. Percentage mode: percent value. Fixed mode: per-item amount."
|
|
681
706
|
}
|
|
682
707
|
},
|
|
683
708
|
{
|
|
@@ -686,7 +711,7 @@ const createReferralProgramsCollection = (pluginConfig) => {
|
|
|
686
711
|
min: 0,
|
|
687
712
|
admin: {
|
|
688
713
|
hidden: true,
|
|
689
|
-
description: "Canonical storage field. Percentage mode: percent. Fixed mode: amount."
|
|
714
|
+
description: "Canonical storage field. Percentage mode: percent value. Fixed mode: per-item amount."
|
|
690
715
|
}
|
|
691
716
|
}
|
|
692
717
|
]
|
|
@@ -873,8 +898,7 @@ function selectBestRuleForItem({ rules, item, itemTotal, quantity, cartTotal, mi
|
|
|
873
898
|
const eligibleRules = rules.filter((rule) => {
|
|
874
899
|
if (!(rule?.totalCommission?.type ? allowedTypes.has(rule.totalCommission.type) : true)) return false;
|
|
875
900
|
const resolvedMinOrderAmount = typeof minOrderAmount === "number" && Number.isFinite(minOrderAmount) ? minOrderAmount : typeof rule?.minOrderAmount === "number" && Number.isFinite(rule.minOrderAmount) ? rule.minOrderAmount : null;
|
|
876
|
-
|
|
877
|
-
if (resolvedMinOrderAmount != null && shouldApplyMinOrder) return cartTotal >= resolvedMinOrderAmount;
|
|
901
|
+
if (resolvedMinOrderAmount != null) return cartTotal >= resolvedMinOrderAmount;
|
|
878
902
|
return true;
|
|
879
903
|
});
|
|
880
904
|
const productId = relationId$5(item.product);
|
|
@@ -924,13 +948,10 @@ function selectBestRuleForItem({ rules, item, itemTotal, quantity, cartTotal, mi
|
|
|
924
948
|
return best;
|
|
925
949
|
}
|
|
926
950
|
function getProgramMinimumOrderAmount({ program, allowedTotalCommissionTypes }) {
|
|
951
|
+
if (typeof program?.minOrderAmount === "number" && Number.isFinite(program.minOrderAmount)) return program.minOrderAmount;
|
|
927
952
|
const rules = Array.isArray(program?.commissionRules) ? program.commissionRules : [];
|
|
928
953
|
if (!rules.length) return null;
|
|
929
954
|
const allowedTypes = allowedCommissionTypesSet(allowedTotalCommissionTypes);
|
|
930
|
-
if (rules.some((rule) => {
|
|
931
|
-
if (rule?.totalCommission?.type) return rule.totalCommission.type === "percentage" && allowedTypes.has(rule.totalCommission.type);
|
|
932
|
-
return true;
|
|
933
|
-
}) && typeof program?.minOrderAmount === "number" && Number.isFinite(program.minOrderAmount)) return program.minOrderAmount;
|
|
934
955
|
const minValues = rules.filter((rule) => {
|
|
935
956
|
if (rule?.totalCommission?.type) return allowedTypes.has(rule.totalCommission.type);
|
|
936
957
|
return true;
|
|
@@ -1174,17 +1195,17 @@ async function handleCouponCode({ payload, cart, cartID, normalizedCode, custome
|
|
|
1174
1195
|
}, { status: 400 });
|
|
1175
1196
|
const cartSubtotal = Number(resolvers.getCartSubtotal(cart)) || 0;
|
|
1176
1197
|
const cartTotal = Number(resolvers.getCartTotal(cart)) || cartSubtotal || 0;
|
|
1177
|
-
if (coupon.minOrderValue &&
|
|
1198
|
+
if (coupon.minOrderValue && cartSubtotal < coupon.minOrderValue) return Response.json({
|
|
1178
1199
|
success: false,
|
|
1179
1200
|
error: `Minimum order value of ${coupon.minOrderValue} ${pluginConfig.defaultCurrency} required`
|
|
1180
1201
|
}, { status: 400 });
|
|
1181
|
-
if (coupon.maxOrderValue &&
|
|
1202
|
+
if (coupon.maxOrderValue && cartSubtotal > coupon.maxOrderValue) return Response.json({
|
|
1182
1203
|
success: false,
|
|
1183
1204
|
error: `Maximum order value of ${coupon.maxOrderValue} ${pluginConfig.defaultCurrency} exceeded`
|
|
1184
1205
|
}, { status: 400 });
|
|
1185
1206
|
const discountAmount = calculateCouponDiscount({
|
|
1186
1207
|
coupon,
|
|
1187
|
-
cartTotal
|
|
1208
|
+
cartTotal: cartSubtotal
|
|
1188
1209
|
});
|
|
1189
1210
|
const nextTotal = roundTo2(Math.max(0, cartTotal - discountAmount));
|
|
1190
1211
|
const data = {};
|
|
@@ -1246,12 +1267,13 @@ async function handleReferralCode({ payload, cart, cartID, normalizedCode, plugi
|
|
|
1246
1267
|
error: "Referral code already applied to this cart"
|
|
1247
1268
|
}, { status: 400 });
|
|
1248
1269
|
const cartItems = resolvers.getCartItems(cart);
|
|
1249
|
-
const
|
|
1270
|
+
const cartSubtotal = Number(resolvers.getCartSubtotal(cart)) || 0;
|
|
1271
|
+
const cartTotal = Number(resolvers.getCartTotal(cart)) || cartSubtotal || 0;
|
|
1250
1272
|
const minOrderAmount = getProgramMinimumOrderAmount({
|
|
1251
1273
|
program,
|
|
1252
1274
|
allowedTotalCommissionTypes: pluginConfig.referralConfig.allowedTotalCommissionTypes
|
|
1253
1275
|
});
|
|
1254
|
-
if (typeof minOrderAmount === "number" &&
|
|
1276
|
+
if (typeof minOrderAmount === "number" && cartSubtotal < minOrderAmount) return Response.json({
|
|
1255
1277
|
success: false,
|
|
1256
1278
|
error: `Minimum order value of ${minOrderAmount} ${pluginConfig.defaultCurrency} required for this referral program`
|
|
1257
1279
|
}, { status: 400 });
|
|
@@ -1259,7 +1281,7 @@ async function handleReferralCode({ payload, cart, cartID, normalizedCode, plugi
|
|
|
1259
1281
|
cartItems,
|
|
1260
1282
|
program,
|
|
1261
1283
|
currencyCode: pluginConfig.defaultCurrency,
|
|
1262
|
-
cartTotal,
|
|
1284
|
+
cartTotal: cartSubtotal,
|
|
1263
1285
|
allowedTotalCommissionTypes: pluginConfig.referralConfig.allowedTotalCommissionTypes
|
|
1264
1286
|
});
|
|
1265
1287
|
const roundedPartnerCommission = roundTo2(partnerCommission);
|
|
@@ -1688,7 +1710,7 @@ async function validateReferralCode({ payload, normalizedCode, cartID, pluginCon
|
|
|
1688
1710
|
id: cartID,
|
|
1689
1711
|
depth: 2
|
|
1690
1712
|
}) : null;
|
|
1691
|
-
const cartTotal = cart ? Number(resolvers.
|
|
1713
|
+
const cartTotal = cart ? Number(resolvers.getCartSubtotal(cart)) || Number(resolvers.getCartTotal(cart)) || 0 : 0;
|
|
1692
1714
|
const minOrderAmount = getProgramMinimumOrderAmount({
|
|
1693
1715
|
program,
|
|
1694
1716
|
allowedTotalCommissionTypes: pluginConfig.referralConfig.allowedTotalCommissionTypes
|