@wtree/payload-ecommerce-coupon 3.79.0 → 3.79.3
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/README.md +1 -0
- package/dist/browser.js +3 -2
- package/dist/browser.js.map +1 -1
- package/dist/browser.mjs +3 -2
- package/dist/browser.mjs.map +1 -1
- package/dist/client/hooks.d.ts.map +1 -1
- package/dist/collections/createCouponsCollection.d.ts +1 -1
- package/dist/collections/createCouponsCollection.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/hooks/recalculateCart.d.ts.map +1 -1
- package/dist/index.js +383 -115
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +383 -115
- package/dist/index.mjs.map +1 -1
- package/dist/plugin.d.ts.map +1 -1
- package/dist/types.d.ts +14 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/utilities/applyCouponContext.d.ts +25 -0
- package/dist/utilities/applyCouponContext.d.ts.map +1 -0
- package/dist/utilities/calculateValues.d.ts +7 -3
- package/dist/utilities/calculateValues.d.ts.map +1 -1
- package/dist/utilities/couponDebug.d.ts +27 -0
- package/dist/utilities/couponDebug.d.ts.map +1 -0
- package/dist/utilities/ecommerceMoney.d.ts +13 -0
- package/dist/utilities/ecommerceMoney.d.ts.map +1 -0
- package/dist/utilities/relationId.d.ts +11 -0
- package/dist/utilities/relationId.d.ts.map +1 -0
- package/dist/utilities/sanitizePluginConfig.d.ts.map +1 -1
- package/package.json +1 -1
package/dist/index.mjs
CHANGED
|
@@ -87,14 +87,14 @@ const createCouponsCollection = (pluginConfig) => {
|
|
|
87
87
|
type: "number",
|
|
88
88
|
required: true,
|
|
89
89
|
admin: {
|
|
90
|
-
description: `If percentage, 10
|
|
90
|
+
description: `If percentage, 10 means 10% (must be 0–100). If fixed, amount in ${defaultCurrency} with up to 2 decimal places (e.g. 10.99).`,
|
|
91
91
|
step: .01
|
|
92
92
|
}
|
|
93
93
|
},
|
|
94
94
|
{
|
|
95
95
|
name: "maxDiscountAmount",
|
|
96
96
|
type: "number",
|
|
97
|
-
admin: { description: `Maximum discount
|
|
97
|
+
admin: { description: `Maximum discount in ${defaultCurrency} (major units, e.g. 20.00). Leave empty for no cap.` }
|
|
98
98
|
},
|
|
99
99
|
{
|
|
100
100
|
name: "usageLimit",
|
|
@@ -119,12 +119,12 @@ const createCouponsCollection = (pluginConfig) => {
|
|
|
119
119
|
{
|
|
120
120
|
name: "minOrderValue",
|
|
121
121
|
type: "number",
|
|
122
|
-
admin: { description: `Minimum
|
|
122
|
+
admin: { description: `Minimum cart subtotal in ${defaultCurrency} (major units, e.g. 50.00)` }
|
|
123
123
|
},
|
|
124
124
|
{
|
|
125
125
|
name: "maxOrderValue",
|
|
126
126
|
type: "number",
|
|
127
|
-
admin: { description: `Maximum
|
|
127
|
+
admin: { description: `Maximum cart subtotal allowed in ${defaultCurrency} (major units, e.g. 500.00)` }
|
|
128
128
|
},
|
|
129
129
|
{
|
|
130
130
|
name: "usageCount",
|
|
@@ -151,6 +151,10 @@ const createCouponsCollection = (pluginConfig) => {
|
|
|
151
151
|
data.code = data.code.trim();
|
|
152
152
|
data.normalizedCode = data.code.toUpperCase();
|
|
153
153
|
}
|
|
154
|
+
if (data?.type === "percentage" && typeof data.value === "number") {
|
|
155
|
+
if (data.value < 0 || data.value > 100) throw new APIError("Percentage coupon value must be between 0 and 100", 400);
|
|
156
|
+
}
|
|
157
|
+
if (data?.type === "fixed" && typeof data.value === "number" && data.value < 0) throw new APIError("Fixed coupon value must be non-negative", 400);
|
|
154
158
|
return data;
|
|
155
159
|
}],
|
|
156
160
|
beforeChange: [({ operation, req, data }) => {
|
|
@@ -706,6 +710,12 @@ const createReferralProgramsCollection = (pluginConfig) => {
|
|
|
706
710
|
timestamps: true
|
|
707
711
|
};
|
|
708
712
|
};
|
|
713
|
+
function minorToMajor2dp(minor) {
|
|
714
|
+
return Math.round(minor) / 100;
|
|
715
|
+
}
|
|
716
|
+
function majorToMinor2dp(major) {
|
|
717
|
+
return Math.round(major * 100);
|
|
718
|
+
}
|
|
709
719
|
function normalizeCurrencyCode(currencyCode) {
|
|
710
720
|
if (!currencyCode) return "AED";
|
|
711
721
|
return currencyCode.toUpperCase();
|
|
@@ -771,7 +781,7 @@ function calculateCouponDiscount({ coupon, cartTotal }) {
|
|
|
771
781
|
}
|
|
772
782
|
return fromCents(discountCents);
|
|
773
783
|
}
|
|
774
|
-
function relationId$
|
|
784
|
+
function relationId$4(value) {
|
|
775
785
|
if (value == null) return null;
|
|
776
786
|
if (typeof value === "string" || typeof value === "number") return value;
|
|
777
787
|
if (typeof value === "object" && (typeof value.id === "string" || typeof value.id === "number")) return value.id;
|
|
@@ -780,7 +790,7 @@ function relationId$5(value) {
|
|
|
780
790
|
const allowedCommissionTypesSet = (allowed) => new Set((allowed && allowed.length ? allowed : ["fixed", "percentage"]).map((v) => v));
|
|
781
791
|
function normalizeIds(values) {
|
|
782
792
|
if (!Array.isArray(values)) return [];
|
|
783
|
-
return values.map(relationId$
|
|
793
|
+
return values.map(relationId$4).filter((v) => v != null);
|
|
784
794
|
}
|
|
785
795
|
function getRuleSplits(rule) {
|
|
786
796
|
const partnerRaw = typeof rule.partnerSplit === "number" ? rule.partnerSplit : typeof rule.referrerSplit === "number" ? rule.referrerSplit : null;
|
|
@@ -894,21 +904,21 @@ function calculateItemRewardByRule({ rule, itemTotalCents, quantity, allowedTota
|
|
|
894
904
|
}
|
|
895
905
|
function getItemCategoryIds(item) {
|
|
896
906
|
const productCategories = Array.isArray(item?.product?.categories) ? normalizeIds(item.product.categories) : [];
|
|
897
|
-
const singleCategory = relationId$
|
|
907
|
+
const singleCategory = relationId$4(item?.category ?? item?.product?.category);
|
|
898
908
|
return [...productCategories, ...singleCategory != null ? [singleCategory] : []];
|
|
899
909
|
}
|
|
900
910
|
function getItemTagIds(item) {
|
|
901
911
|
return Array.isArray(item?.product?.tags) ? normalizeIds(item.product.tags) : [];
|
|
902
912
|
}
|
|
903
|
-
function selectBestRuleForItem({ rules, item, itemTotalCents, quantity, cartTotalCents,
|
|
913
|
+
function selectBestRuleForItem({ rules, item, itemTotalCents, quantity, cartTotalCents, allowedTotalCommissionTypes }) {
|
|
904
914
|
const allowedTypes = allowedCommissionTypesSet(allowedTotalCommissionTypes);
|
|
905
915
|
const eligibleRules = rules.filter((rule) => {
|
|
906
916
|
if (!(rule?.totalCommission?.type ? allowedTypes.has(rule.totalCommission.type) : true)) return false;
|
|
907
|
-
const resolvedMinCents =
|
|
917
|
+
const resolvedMinCents = typeof rule?.minOrderAmount === "number" && rule?.minOrderAmount ? toCents(rule.minOrderAmount) : null;
|
|
908
918
|
if (resolvedMinCents != null) return cartTotalCents >= resolvedMinCents;
|
|
909
919
|
return true;
|
|
910
920
|
});
|
|
911
|
-
const productId = relationId$
|
|
921
|
+
const productId = relationId$4(item.product);
|
|
912
922
|
const itemCategoryIds = new Set(getItemCategoryIds(item));
|
|
913
923
|
const itemTagIds = new Set(getItemTagIds(item));
|
|
914
924
|
const candidates = [
|
|
@@ -973,27 +983,32 @@ function getProgramMinimumOrderAmount({ program, allowedTotalCommissionTypes })
|
|
|
973
983
|
/**
|
|
974
984
|
* Calculate total partner commission and customer discount for a cart.
|
|
975
985
|
*
|
|
976
|
-
*
|
|
977
|
-
*
|
|
986
|
+
* By default, cart line prices and `cartTotal` are **major** currency (e.g. 10.50 = $10.50).
|
|
987
|
+
* When `cartAmountsInMinorUnits` is true, `cartTotal` and line unit prices are **minor**
|
|
988
|
+
* (Payload ecommerce); results are returned in minor units in that mode.
|
|
978
989
|
*/
|
|
979
|
-
function calculateCommissionAndDiscount({ cartItems, program, currencyCode = "AED", cartTotal = 0, allowedTotalCommissionTypes }) {
|
|
990
|
+
function calculateCommissionAndDiscount({ cartItems, program, currencyCode = "AED", cartTotal = 0, allowedTotalCommissionTypes, cartAmountsInMinorUnits = false }) {
|
|
980
991
|
const rules = Array.isArray(program?.commissionRules) ? program.commissionRules : [];
|
|
981
992
|
if (!rules.length) return {
|
|
982
993
|
partnerCommission: 0,
|
|
983
994
|
customerDiscount: 0
|
|
984
995
|
};
|
|
985
|
-
const cartTotalCents = toCents(cartTotal);
|
|
986
|
-
|
|
996
|
+
const cartTotalCents = toCents(cartAmountsInMinorUnits ? minorToMajor2dp(cartTotal) : cartTotal);
|
|
997
|
+
if (typeof program?.minOrderAmount === "number" && Number.isFinite(program.minOrderAmount) && toCents(program.minOrderAmount) > 0 && cartTotalCents < toCents(program.minOrderAmount)) return {
|
|
998
|
+
partnerCommission: 0,
|
|
999
|
+
customerDiscount: 0
|
|
1000
|
+
};
|
|
1001
|
+
const toMajorUnitPrice = (raw) => cartAmountsInMinorUnits ? minorToMajor2dp(raw) : raw;
|
|
987
1002
|
let totalPartnerCents = 0;
|
|
988
1003
|
let totalCustomerCents = 0;
|
|
989
1004
|
for (const item of cartItems) {
|
|
990
1005
|
const product = typeof item.product === "object" ? item.product : {};
|
|
991
|
-
const itemPriceCurrency = getCartItemUnitPrice({
|
|
1006
|
+
const itemPriceCurrency = toMajorUnitPrice(getCartItemUnitPrice({
|
|
992
1007
|
item,
|
|
993
1008
|
product,
|
|
994
1009
|
variant: typeof item.variant === "object" ? item.variant : {},
|
|
995
1010
|
currencyCode
|
|
996
|
-
});
|
|
1011
|
+
}));
|
|
997
1012
|
const quantity = item.quantity ?? 1;
|
|
998
1013
|
const itemTotalCents = toCents(itemPriceCurrency) * quantity;
|
|
999
1014
|
const bestMatch = selectBestRuleForItem({
|
|
@@ -1005,7 +1020,6 @@ function calculateCommissionAndDiscount({ cartItems, program, currencyCode = "AE
|
|
|
1005
1020
|
itemTotalCents,
|
|
1006
1021
|
quantity,
|
|
1007
1022
|
cartTotalCents,
|
|
1008
|
-
minOrderAmountCents: programMinOrderAmountCents,
|
|
1009
1023
|
allowedTotalCommissionTypes
|
|
1010
1024
|
});
|
|
1011
1025
|
if (!bestMatch) continue;
|
|
@@ -1016,10 +1030,96 @@ function calculateCommissionAndDiscount({ cartItems, program, currencyCode = "AE
|
|
|
1016
1030
|
const maxCustomerCents = typeof program?.maxCustomerDiscountPerOrder === "number" && Number.isFinite(program.maxCustomerDiscountPerOrder) ? toCents(program.maxCustomerDiscountPerOrder) : null;
|
|
1017
1031
|
if (maxPartnerCents != null) totalPartnerCents = Math.min(totalPartnerCents, maxPartnerCents);
|
|
1018
1032
|
if (maxCustomerCents != null) totalCustomerCents = Math.min(totalCustomerCents, maxCustomerCents);
|
|
1033
|
+
const partnerMajor = fromCents(totalPartnerCents);
|
|
1034
|
+
const customerMajor = fromCents(totalCustomerCents);
|
|
1035
|
+
if (cartAmountsInMinorUnits) return {
|
|
1036
|
+
partnerCommission: majorToMinor2dp(partnerMajor),
|
|
1037
|
+
customerDiscount: majorToMinor2dp(customerMajor)
|
|
1038
|
+
};
|
|
1019
1039
|
return {
|
|
1020
|
-
partnerCommission:
|
|
1021
|
-
customerDiscount:
|
|
1040
|
+
partnerCommission: partnerMajor,
|
|
1041
|
+
customerDiscount: customerMajor
|
|
1042
|
+
};
|
|
1043
|
+
}
|
|
1044
|
+
//#endregion
|
|
1045
|
+
//#region src/utilities/couponDebug.ts
|
|
1046
|
+
/**
|
|
1047
|
+
* Set DEBUG_COUPON_CART=1 in the environment to log coupon/cart recalculation details
|
|
1048
|
+
* (server console + Payload logger when available).
|
|
1049
|
+
*/
|
|
1050
|
+
function isCouponCartDebugEnabled() {
|
|
1051
|
+
try {
|
|
1052
|
+
return process.env.DEBUG_COUPON_CART === "1" || process.env.DEBUG_COUPON_CART === "true";
|
|
1053
|
+
} catch {
|
|
1054
|
+
return false;
|
|
1055
|
+
}
|
|
1056
|
+
}
|
|
1057
|
+
function logCouponCartDebug(label, data, req) {
|
|
1058
|
+
if (!isCouponCartDebugEnabled()) return;
|
|
1059
|
+
const payload = {
|
|
1060
|
+
msg: `[payload-ecommerce-coupon] ${label}`,
|
|
1061
|
+
...data
|
|
1022
1062
|
};
|
|
1063
|
+
try {
|
|
1064
|
+
req?.payload?.logger?.info(payload);
|
|
1065
|
+
} catch {}
|
|
1066
|
+
console.log("[payload-ecommerce-coupon]", label, data);
|
|
1067
|
+
}
|
|
1068
|
+
/** Detailed server console snapshot for coupon relation resolution (DEBUG_COUPON_CART only). */
|
|
1069
|
+
function logRecalculateCartCouponSnapshot(ctx, req) {
|
|
1070
|
+
if (!isCouponCartDebugEnabled()) return;
|
|
1071
|
+
const f = ctx.cartAppliedCouponField;
|
|
1072
|
+
const fromData = ctx.mutableData[f];
|
|
1073
|
+
const fromOrig = ctx.original[f];
|
|
1074
|
+
const dataHasOwn = Object.prototype.hasOwnProperty.call(ctx.mutableData, f);
|
|
1075
|
+
const origHasOwn = Object.prototype.hasOwnProperty.call(ctx.original, f);
|
|
1076
|
+
const keys = Object.keys(ctx.mutableData);
|
|
1077
|
+
logCouponCartDebug("recalculateCart: coupon snapshot", {
|
|
1078
|
+
couponField: f,
|
|
1079
|
+
dataHasOwnAppliedCoupon: dataHasOwn,
|
|
1080
|
+
origHasOwnAppliedCoupon: origHasOwn,
|
|
1081
|
+
mutableDataAppliedCoupon: fromData,
|
|
1082
|
+
originalAppliedCoupon: fromOrig,
|
|
1083
|
+
effectiveAppliedCoupon: ctx.effectiveAppliedCoupon,
|
|
1084
|
+
appliedCouponID: ctx.appliedCouponID,
|
|
1085
|
+
mutableDataKeysCount: keys.length,
|
|
1086
|
+
mutableDataKeysSample: keys.slice(0, 45)
|
|
1087
|
+
}, req);
|
|
1088
|
+
}
|
|
1089
|
+
//#endregion
|
|
1090
|
+
//#region src/utilities/applyCouponContext.ts
|
|
1091
|
+
/**
|
|
1092
|
+
* When `payload.update` is called from the apply-coupon (or referral) endpoint, that
|
|
1093
|
+
* handler has already computed the correct discount, commission, and total. Setting
|
|
1094
|
+
* this key on `req.context` (with a `SkipRecalculateContext` value) tells
|
|
1095
|
+
* `recalculateCart` to skip its own recalculation and pass the data through unchanged,
|
|
1096
|
+
* while also restoring the relationship IDs that Payload strips during its merge.
|
|
1097
|
+
*/
|
|
1098
|
+
const SKIP_COUPON_RECALCULATE_CONTEXT_KEY = "__payloadEcommerceCouponSkipRecalculate";
|
|
1099
|
+
//#endregion
|
|
1100
|
+
//#region src/utilities/relationId.ts
|
|
1101
|
+
function relationId$3(value) {
|
|
1102
|
+
if (value == null) return null;
|
|
1103
|
+
if (typeof value === "bigint") {
|
|
1104
|
+
const n = Number(value);
|
|
1105
|
+
return Number.isSafeInteger(n) ? n : String(value);
|
|
1106
|
+
}
|
|
1107
|
+
if (typeof value === "string" || typeof value === "number") return value;
|
|
1108
|
+
if (typeof value === "object" && value !== null && "id" in value) {
|
|
1109
|
+
const id = value.id;
|
|
1110
|
+
if (id == null) return null;
|
|
1111
|
+
if (typeof id === "bigint") {
|
|
1112
|
+
const n = Number(id);
|
|
1113
|
+
return Number.isSafeInteger(n) ? n : String(id);
|
|
1114
|
+
}
|
|
1115
|
+
if (typeof id === "string" || typeof id === "number") return id;
|
|
1116
|
+
}
|
|
1117
|
+
return null;
|
|
1118
|
+
}
|
|
1119
|
+
/** Compare two ids that may differ in type (number vs string vs bigint). */
|
|
1120
|
+
function idsEqual(a, b) {
|
|
1121
|
+
if (a == null || b == null) return false;
|
|
1122
|
+
return String(a) === String(b);
|
|
1023
1123
|
}
|
|
1024
1124
|
//#endregion
|
|
1025
1125
|
//#region src/utilities/roundTo2.ts
|
|
@@ -1031,11 +1131,15 @@ function roundTo2(value) {
|
|
|
1031
1131
|
}
|
|
1032
1132
|
//#endregion
|
|
1033
1133
|
//#region src/endpoints/applyCoupon.ts
|
|
1034
|
-
function
|
|
1035
|
-
if (
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1134
|
+
function buildReqWithCartSecret(req, secret) {
|
|
1135
|
+
if (!secret || typeof secret !== "string" || secret.trim().length === 0) return req;
|
|
1136
|
+
return {
|
|
1137
|
+
...req,
|
|
1138
|
+
context: {
|
|
1139
|
+
...req.context || {},
|
|
1140
|
+
cartSecret: secret.trim()
|
|
1141
|
+
}
|
|
1142
|
+
};
|
|
1039
1143
|
}
|
|
1040
1144
|
function readField$3(doc, field) {
|
|
1041
1145
|
if (!doc || typeof doc !== "object") return void 0;
|
|
@@ -1097,6 +1201,8 @@ const applyCouponHandler = ({ pluginConfig }) => async (req) => {
|
|
|
1097
1201
|
const cartIDRaw = data?.cartID;
|
|
1098
1202
|
const cartID = typeof cartIDRaw === "string" || typeof cartIDRaw === "number" ? cartIDRaw : null;
|
|
1099
1203
|
const customerEmail = typeof data?.customerEmail === "string" ? data.customerEmail : void 0;
|
|
1204
|
+
const rawSecret = data?.secret;
|
|
1205
|
+
const reqForCart = buildReqWithCartSecret(req, typeof rawSecret === "string" && rawSecret.trim().length > 0 ? rawSecret.trim() : void 0);
|
|
1100
1206
|
const normalizedCode = normalizeCode$1(rawCode);
|
|
1101
1207
|
if (!normalizedCode || !cartID) return Response.json({
|
|
1102
1208
|
success: false,
|
|
@@ -1120,14 +1226,16 @@ const applyCouponHandler = ({ pluginConfig }) => async (req) => {
|
|
|
1120
1226
|
const cart = await payload.findByID({
|
|
1121
1227
|
collection: collections.cartsSlug,
|
|
1122
1228
|
id: cartID,
|
|
1123
|
-
depth: 2
|
|
1229
|
+
depth: 2,
|
|
1230
|
+
overrideAccess: true,
|
|
1231
|
+
req: reqForCart
|
|
1124
1232
|
});
|
|
1125
1233
|
if (!cart) return Response.json({
|
|
1126
1234
|
success: false,
|
|
1127
1235
|
error: "Cart not found"
|
|
1128
1236
|
}, { status: 404 });
|
|
1129
|
-
const cartAppliedCoupon = relationId$
|
|
1130
|
-
const cartAppliedReferral = relationId$
|
|
1237
|
+
const cartAppliedCoupon = relationId$3(readField$3(cart, fields.cartAppliedCouponField));
|
|
1238
|
+
const cartAppliedReferral = relationId$3(readField$3(cart, fields.cartAppliedReferralCodeField));
|
|
1131
1239
|
if (pluginConfig.referralConfig.singleCodePerCart && (cartAppliedCoupon || cartAppliedReferral)) return Response.json({
|
|
1132
1240
|
success: false,
|
|
1133
1241
|
error: "A code has already been applied to this cart. Only one code can be used per order."
|
|
@@ -1138,7 +1246,8 @@ const applyCouponHandler = ({ pluginConfig }) => async (req) => {
|
|
|
1138
1246
|
cart,
|
|
1139
1247
|
cartID,
|
|
1140
1248
|
normalizedCode,
|
|
1141
|
-
pluginConfig
|
|
1249
|
+
pluginConfig,
|
|
1250
|
+
reqForCart
|
|
1142
1251
|
});
|
|
1143
1252
|
if (!referralResult.ok && referralResult.status === 404 && pluginConfig.referralConfig.allowBothSystems && allowCoupon) return await handleCouponCode({
|
|
1144
1253
|
payload,
|
|
@@ -1146,7 +1255,9 @@ const applyCouponHandler = ({ pluginConfig }) => async (req) => {
|
|
|
1146
1255
|
cartID,
|
|
1147
1256
|
normalizedCode,
|
|
1148
1257
|
customerEmail,
|
|
1149
|
-
pluginConfig
|
|
1258
|
+
pluginConfig,
|
|
1259
|
+
req,
|
|
1260
|
+
reqForCart
|
|
1150
1261
|
});
|
|
1151
1262
|
return referralResult;
|
|
1152
1263
|
}
|
|
@@ -1160,7 +1271,9 @@ const applyCouponHandler = ({ pluginConfig }) => async (req) => {
|
|
|
1160
1271
|
cartID,
|
|
1161
1272
|
normalizedCode,
|
|
1162
1273
|
customerEmail,
|
|
1163
|
-
pluginConfig
|
|
1274
|
+
pluginConfig,
|
|
1275
|
+
req,
|
|
1276
|
+
reqForCart
|
|
1164
1277
|
});
|
|
1165
1278
|
} catch (error) {
|
|
1166
1279
|
console.error("Code application error:", error);
|
|
@@ -1170,9 +1283,10 @@ const applyCouponHandler = ({ pluginConfig }) => async (req) => {
|
|
|
1170
1283
|
}, { status: 500 });
|
|
1171
1284
|
}
|
|
1172
1285
|
};
|
|
1173
|
-
async function handleCouponCode({ payload, cart, cartID, normalizedCode, customerEmail, pluginConfig }) {
|
|
1286
|
+
async function handleCouponCode({ payload, cart, cartID, normalizedCode, customerEmail, pluginConfig, req, reqForCart }) {
|
|
1174
1287
|
const fields = pluginConfig.integration.fields;
|
|
1175
1288
|
const resolvers = pluginConfig.integration.resolvers;
|
|
1289
|
+
const cartMinor = pluginConfig.integration?.cartAmountsInMinorUnits === true;
|
|
1176
1290
|
const coupon = await findByNormalizedCode$1({
|
|
1177
1291
|
payload,
|
|
1178
1292
|
collection: pluginConfig.collections.couponsSlug,
|
|
@@ -1216,34 +1330,85 @@ async function handleCouponCode({ payload, cart, cartID, normalizedCode, custome
|
|
|
1216
1330
|
error: "You have reached the maximum uses for this coupon."
|
|
1217
1331
|
}, { status: 400 });
|
|
1218
1332
|
}
|
|
1219
|
-
if (relationId$
|
|
1333
|
+
if (idsEqual(relationId$3(readField$3(cart, fields.cartAppliedCouponField)), coupon.id)) return Response.json({
|
|
1220
1334
|
success: false,
|
|
1221
1335
|
error: "Coupon already applied to this cart"
|
|
1222
1336
|
}, { status: 400 });
|
|
1223
|
-
const
|
|
1224
|
-
|
|
1225
|
-
|
|
1337
|
+
const referralOnCart = relationId$3(readField$3(cart, fields.cartAppliedReferralCodeField)) != null;
|
|
1338
|
+
if (pluginConfig.enableReferrals && !pluginConfig.referralConfig.allowBothSystems && referralOnCart) {
|
|
1339
|
+
logCouponCartDebug("applyCoupon: rejected — referral on cart (allowBothSystems=false)", {
|
|
1340
|
+
cartID,
|
|
1341
|
+
referralOnCart,
|
|
1342
|
+
couponId: coupon.id
|
|
1343
|
+
}, req);
|
|
1344
|
+
return Response.json({
|
|
1345
|
+
success: false,
|
|
1346
|
+
error: "A referral code is already applied to this cart. Remove it before applying a coupon."
|
|
1347
|
+
}, { status: 400 });
|
|
1348
|
+
}
|
|
1349
|
+
const cartSubtotalRaw = Number(resolvers.getCartSubtotal(cart)) || 0;
|
|
1350
|
+
const cartSubtotalMajor = cartMinor ? minorToMajor2dp(cartSubtotalRaw) : cartSubtotalRaw;
|
|
1351
|
+
const cartTotalRaw = Number(resolvers.getCartTotal(cart)) || cartSubtotalRaw || 0;
|
|
1352
|
+
if (coupon.minOrderValue && cartSubtotalMajor < coupon.minOrderValue) return Response.json({
|
|
1226
1353
|
success: false,
|
|
1227
1354
|
error: `Minimum order value of ${coupon.minOrderValue} ${pluginConfig.defaultCurrency} required`
|
|
1228
1355
|
}, { status: 400 });
|
|
1229
|
-
if (coupon.maxOrderValue &&
|
|
1356
|
+
if (coupon.maxOrderValue && cartSubtotalMajor > coupon.maxOrderValue) return Response.json({
|
|
1230
1357
|
success: false,
|
|
1231
1358
|
error: `Maximum order value of ${coupon.maxOrderValue} ${pluginConfig.defaultCurrency} exceeded`
|
|
1232
1359
|
}, { status: 400 });
|
|
1233
|
-
const
|
|
1360
|
+
const discountMajor = calculateCouponDiscount({
|
|
1234
1361
|
coupon,
|
|
1235
|
-
cartTotal:
|
|
1362
|
+
cartTotal: cartSubtotalMajor
|
|
1236
1363
|
});
|
|
1237
|
-
const
|
|
1364
|
+
const discountAmount = cartMinor ? majorToMinor2dp(discountMajor) : discountMajor;
|
|
1365
|
+
const nextTotal = cartMinor ? Math.max(0, Math.round(cartTotalRaw) - Math.round(discountAmount)) : roundTo2(Math.max(0, cartTotalRaw - discountAmount));
|
|
1238
1366
|
const data = {};
|
|
1239
1367
|
writeField$1(data, fields.cartAppliedCouponField, coupon.id);
|
|
1240
1368
|
writeField$1(data, fields.cartDiscountAmountField, discountAmount);
|
|
1241
1369
|
writeField$1(data, fields.cartTotalField, nextTotal);
|
|
1370
|
+
reqForCart.context = {
|
|
1371
|
+
...reqForCart.context || {},
|
|
1372
|
+
[SKIP_COUPON_RECALCULATE_CONTEXT_KEY]: { couponId: coupon.id }
|
|
1373
|
+
};
|
|
1242
1374
|
await payload.update({
|
|
1243
1375
|
collection: pluginConfig.integration.collections.cartsSlug,
|
|
1244
1376
|
id: cartID,
|
|
1245
|
-
data
|
|
1377
|
+
data,
|
|
1378
|
+
overrideAccess: true,
|
|
1379
|
+
req: reqForCart
|
|
1246
1380
|
});
|
|
1381
|
+
if (isCouponCartDebugEnabled()) try {
|
|
1382
|
+
const cartAfter = await payload.findByID({
|
|
1383
|
+
collection: pluginConfig.integration.collections.cartsSlug,
|
|
1384
|
+
id: cartID,
|
|
1385
|
+
depth: 0,
|
|
1386
|
+
overrideAccess: true,
|
|
1387
|
+
req: reqForCart
|
|
1388
|
+
});
|
|
1389
|
+
console.log("[payload-ecommerce-coupon] applyCoupon: cart after update", {
|
|
1390
|
+
cartID,
|
|
1391
|
+
wroteAppliedCoupon: data[fields.cartAppliedCouponField],
|
|
1392
|
+
appliedCouponOnDoc: readField$3(cartAfter, fields.cartAppliedCouponField),
|
|
1393
|
+
discountAmountOnDoc: readField$3(cartAfter, fields.cartDiscountAmountField),
|
|
1394
|
+
totalOnDoc: readField$3(cartAfter, fields.cartTotalField)
|
|
1395
|
+
});
|
|
1396
|
+
} catch (err) {
|
|
1397
|
+
console.log("[payload-ecommerce-coupon] applyCoupon: cart after update (read failed)", err);
|
|
1398
|
+
}
|
|
1399
|
+
logCouponCartDebug("applyCoupon: success", {
|
|
1400
|
+
cartID,
|
|
1401
|
+
cartMinor,
|
|
1402
|
+
cartSubtotalRaw,
|
|
1403
|
+
cartSubtotalMajor,
|
|
1404
|
+
cartTotalRaw,
|
|
1405
|
+
discountMajor,
|
|
1406
|
+
discountAmount,
|
|
1407
|
+
nextTotal,
|
|
1408
|
+
couponId: coupon.id,
|
|
1409
|
+
couponType: coupon.type,
|
|
1410
|
+
couponValue: coupon.value
|
|
1411
|
+
}, req);
|
|
1247
1412
|
return Response.json({
|
|
1248
1413
|
success: true,
|
|
1249
1414
|
message: "Coupon applied successfully",
|
|
@@ -1256,9 +1421,10 @@ async function handleCouponCode({ payload, cart, cartID, normalizedCode, custome
|
|
|
1256
1421
|
currency: pluginConfig.defaultCurrency
|
|
1257
1422
|
});
|
|
1258
1423
|
}
|
|
1259
|
-
async function handleReferralCode({ payload, cart, cartID, normalizedCode, pluginConfig }) {
|
|
1424
|
+
async function handleReferralCode({ payload, cart, cartID, normalizedCode, pluginConfig, reqForCart }) {
|
|
1260
1425
|
const fields = pluginConfig.integration.fields;
|
|
1261
1426
|
const resolvers = pluginConfig.integration.resolvers;
|
|
1427
|
+
const cartMinor = pluginConfig.integration?.cartAmountsInMinorUnits === true;
|
|
1262
1428
|
const referralCode = await findByNormalizedCode$1({
|
|
1263
1429
|
payload,
|
|
1264
1430
|
collection: pluginConfig.collections.referralCodesSlug,
|
|
@@ -1289,18 +1455,19 @@ async function handleReferralCode({ payload, cart, cartID, normalizedCode, plugi
|
|
|
1289
1455
|
success: false,
|
|
1290
1456
|
error: "Referral program is not active"
|
|
1291
1457
|
}, { status: 400 });
|
|
1292
|
-
if (relationId$
|
|
1458
|
+
if (idsEqual(relationId$3(readField$3(cart, fields.cartAppliedReferralCodeField)), referralCode.id)) return Response.json({
|
|
1293
1459
|
success: false,
|
|
1294
1460
|
error: "Referral code already applied to this cart"
|
|
1295
1461
|
}, { status: 400 });
|
|
1296
1462
|
const cartItems = resolvers.getCartItems(cart);
|
|
1297
1463
|
const cartSubtotal = Number(resolvers.getCartSubtotal(cart)) || 0;
|
|
1464
|
+
const cartSubtotalMajor = cartMinor ? minorToMajor2dp(cartSubtotal) : cartSubtotal;
|
|
1298
1465
|
const cartTotal = Number(resolvers.getCartTotal(cart)) || cartSubtotal || 0;
|
|
1299
1466
|
const minOrderAmount = getProgramMinimumOrderAmount({
|
|
1300
1467
|
program,
|
|
1301
1468
|
allowedTotalCommissionTypes: pluginConfig.referralConfig.allowedTotalCommissionTypes
|
|
1302
1469
|
});
|
|
1303
|
-
if (typeof minOrderAmount === "number" &&
|
|
1470
|
+
if (typeof minOrderAmount === "number" && cartSubtotalMajor < minOrderAmount) return Response.json({
|
|
1304
1471
|
success: false,
|
|
1305
1472
|
error: `Minimum order value of ${minOrderAmount} ${pluginConfig.defaultCurrency} required for this referral program`
|
|
1306
1473
|
}, { status: 400 });
|
|
@@ -1309,20 +1476,27 @@ async function handleReferralCode({ payload, cart, cartID, normalizedCode, plugi
|
|
|
1309
1476
|
program,
|
|
1310
1477
|
currencyCode: pluginConfig.defaultCurrency,
|
|
1311
1478
|
cartTotal: cartSubtotal,
|
|
1312
|
-
allowedTotalCommissionTypes: pluginConfig.referralConfig.allowedTotalCommissionTypes
|
|
1479
|
+
allowedTotalCommissionTypes: pluginConfig.referralConfig.allowedTotalCommissionTypes,
|
|
1480
|
+
cartAmountsInMinorUnits: cartMinor
|
|
1313
1481
|
});
|
|
1314
1482
|
const roundedPartnerCommission = roundTo2(partnerCommission);
|
|
1315
1483
|
const roundedCustomerDiscount = roundTo2(customerDiscount);
|
|
1316
|
-
const nextTotal = roundTo2(Math.max(0, cartTotal - roundedCustomerDiscount));
|
|
1484
|
+
const nextTotal = cartMinor ? Math.max(0, Math.round(cartTotal) - Math.round(roundedCustomerDiscount)) : roundTo2(Math.max(0, cartTotal - roundedCustomerDiscount));
|
|
1317
1485
|
const data = {};
|
|
1318
1486
|
writeField$1(data, fields.cartAppliedReferralCodeField, referralCode.id);
|
|
1319
1487
|
writeField$1(data, fields.cartPartnerCommissionField, roundedPartnerCommission);
|
|
1320
1488
|
writeField$1(data, fields.cartCustomerDiscountField, roundedCustomerDiscount);
|
|
1321
1489
|
writeField$1(data, fields.cartTotalField, nextTotal);
|
|
1490
|
+
reqForCart.context = {
|
|
1491
|
+
...reqForCart.context || {},
|
|
1492
|
+
[SKIP_COUPON_RECALCULATE_CONTEXT_KEY]: { referralId: referralCode.id }
|
|
1493
|
+
};
|
|
1322
1494
|
await payload.update({
|
|
1323
1495
|
collection: pluginConfig.integration.collections.cartsSlug,
|
|
1324
1496
|
id: cartID,
|
|
1325
|
-
data
|
|
1497
|
+
data,
|
|
1498
|
+
overrideAccess: true,
|
|
1499
|
+
req: reqForCart
|
|
1326
1500
|
});
|
|
1327
1501
|
return Response.json({
|
|
1328
1502
|
success: true,
|
|
@@ -1340,7 +1514,7 @@ const applyCouponEndpoint = ({ pluginConfig }) => ({
|
|
|
1340
1514
|
});
|
|
1341
1515
|
//#endregion
|
|
1342
1516
|
//#region src/endpoints/partnerStats.ts
|
|
1343
|
-
function relationId$
|
|
1517
|
+
function relationId$2(value) {
|
|
1344
1518
|
if (value == null) return null;
|
|
1345
1519
|
if (typeof value === "string" || typeof value === "number") return value;
|
|
1346
1520
|
if (typeof value === "object" && (typeof value.id === "string" || typeof value.id === "number")) return value.id;
|
|
@@ -1424,7 +1598,7 @@ const partnerStatsHandler = ({ pluginConfig }) => async (req) => {
|
|
|
1424
1598
|
const conversionRate = totalReferrals > 0 ? successfulReferrals / totalReferrals * 100 : 0;
|
|
1425
1599
|
const recentReferrals = [];
|
|
1426
1600
|
try {
|
|
1427
|
-
const referralCodeIDs = referralCodes.map((c) => relationId$
|
|
1601
|
+
const referralCodeIDs = referralCodes.map((c) => relationId$2(c?.id)).filter((id) => id != null);
|
|
1428
1602
|
if (referralCodeIDs.length > 0) {
|
|
1429
1603
|
const ordersQuery = await payload.find({
|
|
1430
1604
|
collection: collections.ordersSlug,
|
|
@@ -1433,8 +1607,8 @@ const partnerStatsHandler = ({ pluginConfig }) => async (req) => {
|
|
|
1433
1607
|
sort: `-${fields.orderCreatedAtField}`
|
|
1434
1608
|
});
|
|
1435
1609
|
for (const order of ordersQuery?.docs || []) {
|
|
1436
|
-
const orderReferralID = relationId$
|
|
1437
|
-
const matchedCode = referralCodes.find((c) => relationId$
|
|
1610
|
+
const orderReferralID = relationId$2(readField$2(order, fields.orderAppliedReferralCodeField));
|
|
1611
|
+
const matchedCode = referralCodes.find((c) => relationId$2(c?.id) === orderReferralID);
|
|
1438
1612
|
const paymentStatus = readField$2(order, fields.orderPaymentStatusField);
|
|
1439
1613
|
const createdAt = readField$2(order, fields.orderCreatedAtField);
|
|
1440
1614
|
recentReferrals.push({
|
|
@@ -1464,7 +1638,7 @@ const partnerStatsHandler = ({ pluginConfig }) => async (req) => {
|
|
|
1464
1638
|
let program = null;
|
|
1465
1639
|
if (referralCodes.length > 0) {
|
|
1466
1640
|
const firstCode = referralCodes[0];
|
|
1467
|
-
const programID = relationId$
|
|
1641
|
+
const programID = relationId$2(firstCode?.program);
|
|
1468
1642
|
if (programID != null) try {
|
|
1469
1643
|
const programData = await payload.findByID({
|
|
1470
1644
|
collection: pluginConfig.collections.referralProgramsSlug,
|
|
@@ -1517,7 +1691,7 @@ const partnerStatsEndpoint = ({ pluginConfig }) => ({
|
|
|
1517
1691
|
});
|
|
1518
1692
|
//#endregion
|
|
1519
1693
|
//#region src/endpoints/validateCoupon.ts
|
|
1520
|
-
function relationId$
|
|
1694
|
+
function relationId$1(value) {
|
|
1521
1695
|
if (value == null) return null;
|
|
1522
1696
|
if (typeof value === "string" || typeof value === "number") return value;
|
|
1523
1697
|
if (typeof value === "object" && (typeof value.id === "string" || typeof value.id === "number")) return value.id;
|
|
@@ -1622,6 +1796,7 @@ const validateCouponHandler = ({ pluginConfig }) => async (req) => {
|
|
|
1622
1796
|
};
|
|
1623
1797
|
async function validateCouponCode$1({ payload, normalizedCode, cartValue, customerEmail, pluginConfig }) {
|
|
1624
1798
|
const fields = pluginConfig.integration.fields;
|
|
1799
|
+
const cartMinor = pluginConfig.integration?.cartAmountsInMinorUnits === true;
|
|
1625
1800
|
const couponData = await findByNormalizedCode({
|
|
1626
1801
|
payload,
|
|
1627
1802
|
collection: pluginConfig.collections.couponsSlug,
|
|
@@ -1664,24 +1839,23 @@ async function validateCouponCode$1({ payload, normalizedCode, cartValue, custom
|
|
|
1664
1839
|
if (cartValue !== void 0) {
|
|
1665
1840
|
const minOrderValue = couponData.minOrderValue;
|
|
1666
1841
|
const maxOrderValue = couponData.maxOrderValue;
|
|
1667
|
-
|
|
1842
|
+
const cartValueMajor = cartMinor ? minorToMajor2dp(cartValue) : cartValue;
|
|
1843
|
+
if (minOrderValue && cartValueMajor < minOrderValue) return Response.json({
|
|
1668
1844
|
success: false,
|
|
1669
1845
|
error: `Minimum order value of ${minOrderValue} ${pluginConfig.defaultCurrency} required`
|
|
1670
1846
|
}, { status: 400 });
|
|
1671
|
-
if (maxOrderValue &&
|
|
1847
|
+
if (maxOrderValue && cartValueMajor > maxOrderValue) return Response.json({
|
|
1672
1848
|
success: false,
|
|
1673
1849
|
error: `Maximum order value of ${maxOrderValue} ${pluginConfig.defaultCurrency} exceeded`
|
|
1674
1850
|
}, { status: 400 });
|
|
1675
1851
|
}
|
|
1676
1852
|
let discount = 0;
|
|
1677
1853
|
if (cartValue !== void 0) {
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
}
|
|
1682
|
-
|
|
1683
|
-
if (discount > cartValue) discount = roundTo2(cartValue);
|
|
1684
|
-
}
|
|
1854
|
+
const discountMajor = calculateCouponDiscount({
|
|
1855
|
+
coupon: couponData,
|
|
1856
|
+
cartTotal: cartMinor ? minorToMajor2dp(cartValue) : cartValue
|
|
1857
|
+
});
|
|
1858
|
+
discount = cartMinor ? majorToMinor2dp(discountMajor) : discountMajor;
|
|
1685
1859
|
}
|
|
1686
1860
|
return Response.json({
|
|
1687
1861
|
success: true,
|
|
@@ -1698,6 +1872,7 @@ async function validateCouponCode$1({ payload, normalizedCode, cartValue, custom
|
|
|
1698
1872
|
async function validateReferralCode({ payload, normalizedCode, cartID, pluginConfig }) {
|
|
1699
1873
|
const collections = pluginConfig.integration.collections;
|
|
1700
1874
|
const resolvers = pluginConfig.integration.resolvers;
|
|
1875
|
+
const cartMinor = pluginConfig.integration?.cartAmountsInMinorUnits === true;
|
|
1701
1876
|
const referralData = await findByNormalizedCode({
|
|
1702
1877
|
payload,
|
|
1703
1878
|
collection: pluginConfig.collections.referralCodesSlug,
|
|
@@ -1719,7 +1894,7 @@ async function validateReferralCode({ payload, normalizedCode, cartID, pluginCon
|
|
|
1719
1894
|
success: false,
|
|
1720
1895
|
error: "Referral code usage limit exceeded"
|
|
1721
1896
|
}, { status: 400 });
|
|
1722
|
-
const programId = relationId$
|
|
1897
|
+
const programId = relationId$1(referralData.program);
|
|
1723
1898
|
if (programId == null) return Response.json({
|
|
1724
1899
|
success: false,
|
|
1725
1900
|
error: "Referral program not found"
|
|
@@ -1737,12 +1912,13 @@ async function validateReferralCode({ payload, normalizedCode, cartID, pluginCon
|
|
|
1737
1912
|
id: cartID,
|
|
1738
1913
|
depth: 2
|
|
1739
1914
|
}) : null;
|
|
1740
|
-
const
|
|
1915
|
+
const cartSubtotal = cart ? Number(resolvers.getCartSubtotal(cart)) || Number(resolvers.getCartTotal(cart)) || 0 : 0;
|
|
1916
|
+
const cartTotalMajor = cartMinor ? minorToMajor2dp(cartSubtotal) : cartSubtotal;
|
|
1741
1917
|
const minOrderAmount = getProgramMinimumOrderAmount({
|
|
1742
1918
|
program,
|
|
1743
1919
|
allowedTotalCommissionTypes: pluginConfig.referralConfig.allowedTotalCommissionTypes
|
|
1744
1920
|
});
|
|
1745
|
-
if (typeof minOrderAmount === "number" &&
|
|
1921
|
+
if (typeof minOrderAmount === "number" && cartTotalMajor < minOrderAmount) return Response.json({
|
|
1746
1922
|
success: false,
|
|
1747
1923
|
error: `Minimum order value of ${minOrderAmount} ${pluginConfig.defaultCurrency} required for this referral program`
|
|
1748
1924
|
}, { status: 400 });
|
|
@@ -1750,17 +1926,19 @@ async function validateReferralCode({ payload, normalizedCode, cartID, pluginCon
|
|
|
1750
1926
|
cartItems: cart ? resolvers.getCartItems(cart) : [],
|
|
1751
1927
|
program,
|
|
1752
1928
|
currencyCode: pluginConfig.defaultCurrency,
|
|
1753
|
-
cartTotal,
|
|
1754
|
-
allowedTotalCommissionTypes: pluginConfig.referralConfig.allowedTotalCommissionTypes
|
|
1929
|
+
cartTotal: cartSubtotal,
|
|
1930
|
+
allowedTotalCommissionTypes: pluginConfig.referralConfig.allowedTotalCommissionTypes,
|
|
1931
|
+
cartAmountsInMinorUnits: cartMinor
|
|
1755
1932
|
});
|
|
1756
|
-
const cappedCustomerDiscount =
|
|
1933
|
+
const cappedCustomerDiscount = cartSubtotal > 0 ? Math.min(customerDiscount, cartSubtotal) : customerDiscount;
|
|
1757
1934
|
const roundedPartnerCommission = roundTo2(partnerCommission);
|
|
1758
1935
|
const roundedCustomerDiscount = roundTo2(cappedCustomerDiscount);
|
|
1936
|
+
const displayDiscount = cartMinor ? minorToMajor2dp(roundedCustomerDiscount) : roundedCustomerDiscount;
|
|
1759
1937
|
return Response.json({
|
|
1760
1938
|
success: true,
|
|
1761
1939
|
referralCode: {
|
|
1762
1940
|
code: referralData.code,
|
|
1763
|
-
description: `Get ${
|
|
1941
|
+
description: `Get ${displayDiscount.toFixed(2)} discount with this referral code`
|
|
1764
1942
|
},
|
|
1765
1943
|
partnerCommission: roundedPartnerCommission,
|
|
1766
1944
|
customerDiscount: roundedCustomerDiscount,
|
|
@@ -1774,16 +1952,24 @@ const validateCouponEndpoint = ({ pluginConfig }) => ({
|
|
|
1774
1952
|
});
|
|
1775
1953
|
//#endregion
|
|
1776
1954
|
//#region src/hooks/recalculateCart.ts
|
|
1777
|
-
function relationId$1(value) {
|
|
1778
|
-
if (value == null) return null;
|
|
1779
|
-
if (typeof value === "string" || typeof value === "number") return value;
|
|
1780
|
-
if (typeof value === "object" && (typeof value.id === "string" || typeof value.id === "number")) return value.id;
|
|
1781
|
-
return null;
|
|
1782
|
-
}
|
|
1783
1955
|
function readField$1(doc, field) {
|
|
1784
1956
|
if (!doc || typeof doc !== "object") return void 0;
|
|
1785
1957
|
return doc[field];
|
|
1786
1958
|
}
|
|
1959
|
+
/**
|
|
1960
|
+
* Prefer incoming patch when that field is present on `data`; otherwise use `originalDoc`.
|
|
1961
|
+
* If the key exists but the value is `undefined` (common with merged Payload update objects),
|
|
1962
|
+
* treat it like a missing patch and fall back to `originalDoc`.
|
|
1963
|
+
* Explicit `null` still means "clear relation".
|
|
1964
|
+
*/
|
|
1965
|
+
function effectiveRelationField(mutableData, original, fieldName) {
|
|
1966
|
+
if (Object.prototype.hasOwnProperty.call(mutableData, fieldName)) {
|
|
1967
|
+
const v = readField$1(mutableData, fieldName);
|
|
1968
|
+
if (v === void 0) return readField$1(original, fieldName);
|
|
1969
|
+
return v;
|
|
1970
|
+
}
|
|
1971
|
+
return readField$1(original, fieldName);
|
|
1972
|
+
}
|
|
1787
1973
|
function writeField(doc, field, value) {
|
|
1788
1974
|
doc[field] = value;
|
|
1789
1975
|
}
|
|
@@ -1798,6 +1984,24 @@ function clearReferralFields(target, fields) {
|
|
|
1798
1984
|
}
|
|
1799
1985
|
const recalculateCartHook = (pluginConfig) => async ({ data, req, originalDoc }) => {
|
|
1800
1986
|
if (!req.payload) return data;
|
|
1987
|
+
const skipCtx = req?.context?.[SKIP_COUPON_RECALCULATE_CONTEXT_KEY];
|
|
1988
|
+
if (skipCtx != null) {
|
|
1989
|
+
const mutableData = data || {};
|
|
1990
|
+
if (typeof skipCtx === "object") {
|
|
1991
|
+
const { couponId, referralId } = skipCtx;
|
|
1992
|
+
const f = pluginConfig.integration?.fields;
|
|
1993
|
+
const couponField = f?.cartAppliedCouponField ?? "appliedCoupon";
|
|
1994
|
+
const referralField = f?.cartAppliedReferralCodeField ?? "appliedReferralCode";
|
|
1995
|
+
if (couponId != null) mutableData[couponField] = couponId;
|
|
1996
|
+
if (referralId != null) mutableData[referralField] = referralId;
|
|
1997
|
+
logCouponCartDebug("recalculateCart: skip flag active — passing through and restoring IDs", {
|
|
1998
|
+
couponId: couponId ?? null,
|
|
1999
|
+
referralId: referralId ?? null
|
|
2000
|
+
}, req);
|
|
2001
|
+
}
|
|
2002
|
+
return mutableData;
|
|
2003
|
+
}
|
|
2004
|
+
const cartMinor = pluginConfig.integration?.cartAmountsInMinorUnits === true;
|
|
1801
2005
|
const integration = pluginConfig.integration || {};
|
|
1802
2006
|
const collections = integration.collections || {
|
|
1803
2007
|
cartsSlug: "carts",
|
|
@@ -1872,16 +2076,26 @@ const recalculateCartHook = (pluginConfig) => async ({ data, req, originalDoc })
|
|
|
1872
2076
|
};
|
|
1873
2077
|
const mutableData = data || {};
|
|
1874
2078
|
const original = originalDoc || {};
|
|
2079
|
+
const rawCurrency = readField$1(mutableData, "currency") ?? readField$1(original, "currency");
|
|
2080
|
+
const cartCurrency = typeof rawCurrency === "string" && rawCurrency.trim().length > 0 ? rawCurrency.trim() : pluginConfig.defaultCurrency;
|
|
1875
2081
|
const effectiveItems = readField$1(mutableData, fields.cartItemsField) ?? readField$1(original, fields.cartItemsField) ?? [];
|
|
1876
|
-
const effectiveAppliedReferral =
|
|
1877
|
-
const effectiveAppliedCoupon =
|
|
2082
|
+
const effectiveAppliedReferral = effectiveRelationField(mutableData, original, fields.cartAppliedReferralCodeField);
|
|
2083
|
+
const effectiveAppliedCoupon = effectiveRelationField(mutableData, original, fields.cartAppliedCouponField);
|
|
2084
|
+
const appliedCouponID = relationId$3(effectiveAppliedCoupon);
|
|
2085
|
+
logRecalculateCartCouponSnapshot({
|
|
2086
|
+
cartAppliedCouponField: fields.cartAppliedCouponField,
|
|
2087
|
+
mutableData,
|
|
2088
|
+
original,
|
|
2089
|
+
effectiveAppliedCoupon,
|
|
2090
|
+
appliedCouponID
|
|
2091
|
+
}, req);
|
|
1878
2092
|
if (!Array.isArray(effectiveItems) || effectiveItems.length === 0) {
|
|
1879
2093
|
clearReferralFields(mutableData, fields);
|
|
1880
2094
|
clearCouponFields(mutableData, fields);
|
|
1881
2095
|
writeField(mutableData, fields.cartTotalField, 0);
|
|
1882
2096
|
return mutableData;
|
|
1883
2097
|
}
|
|
1884
|
-
const getRelationID = (value) => relationId$
|
|
2098
|
+
const getRelationID = (value) => relationId$3(value);
|
|
1885
2099
|
const productIds = effectiveItems.map((item) => getRelationID(item?.product)).filter((id) => id != null);
|
|
1886
2100
|
let productsMap = /* @__PURE__ */ new Map();
|
|
1887
2101
|
if (productIds.length > 0) {
|
|
@@ -1901,7 +2115,7 @@ const recalculateCartHook = (pluginConfig) => async ({ data, req, originalDoc })
|
|
|
1901
2115
|
item,
|
|
1902
2116
|
product,
|
|
1903
2117
|
variant,
|
|
1904
|
-
currencyCode:
|
|
2118
|
+
currencyCode: cartCurrency
|
|
1905
2119
|
}));
|
|
1906
2120
|
const quantity = typeof item?.quantity === "number" && Number.isFinite(item.quantity) ? item.quantity : 1;
|
|
1907
2121
|
const safeUnitPrice = Number.isFinite(unitPrice) ? unitPrice : 0;
|
|
@@ -1913,10 +2127,11 @@ const recalculateCartHook = (pluginConfig) => async ({ data, req, originalDoc })
|
|
|
1913
2127
|
quantity
|
|
1914
2128
|
};
|
|
1915
2129
|
});
|
|
1916
|
-
|
|
2130
|
+
const subtotalMajor = cartMinor ? minorToMajor2dp(calculatedSubtotal) : calculatedSubtotal;
|
|
2131
|
+
writeField(mutableData, fields.cartSubtotalField, cartMinor ? Math.round(calculatedSubtotal) : roundTo2(calculatedSubtotal));
|
|
1917
2132
|
let customerDiscount = 0;
|
|
1918
2133
|
let couponDiscount = 0;
|
|
1919
|
-
const appliedReferralID = relationId$
|
|
2134
|
+
const appliedReferralID = relationId$3(effectiveAppliedReferral);
|
|
1920
2135
|
if (pluginConfig.enableReferrals && appliedReferralID != null) {
|
|
1921
2136
|
const referralCode = (await req.payload.find({
|
|
1922
2137
|
collection: pluginConfig.collections.referralCodesSlug,
|
|
@@ -1926,7 +2141,7 @@ const recalculateCartHook = (pluginConfig) => async ({ data, req, originalDoc })
|
|
|
1926
2141
|
}))?.docs?.[0];
|
|
1927
2142
|
if (!referralCode || referralCode.isActive === false) clearReferralFields(mutableData, fields);
|
|
1928
2143
|
else {
|
|
1929
|
-
const programId = relationId$
|
|
2144
|
+
const programId = relationId$3(referralCode.program);
|
|
1930
2145
|
const program = typeof referralCode.program === "object" ? referralCode.program : programId != null ? await req.payload.findByID({
|
|
1931
2146
|
collection: pluginConfig.collections.referralProgramsSlug,
|
|
1932
2147
|
id: programId
|
|
@@ -1937,14 +2152,15 @@ const recalculateCartHook = (pluginConfig) => async ({ data, req, originalDoc })
|
|
|
1937
2152
|
program,
|
|
1938
2153
|
allowedTotalCommissionTypes: pluginConfig.referralConfig.allowedTotalCommissionTypes
|
|
1939
2154
|
});
|
|
1940
|
-
if (typeof minOrderAmount === "number" &&
|
|
2155
|
+
if (typeof minOrderAmount === "number" && subtotalMajor < minOrderAmount) clearReferralFields(mutableData, fields);
|
|
1941
2156
|
else {
|
|
1942
2157
|
const result = calculateCommissionAndDiscount({
|
|
1943
2158
|
cartItems: enrichedItems,
|
|
1944
2159
|
program,
|
|
1945
|
-
currencyCode:
|
|
2160
|
+
currencyCode: cartCurrency,
|
|
1946
2161
|
cartTotal: calculatedSubtotal,
|
|
1947
|
-
allowedTotalCommissionTypes: pluginConfig.referralConfig.allowedTotalCommissionTypes
|
|
2162
|
+
allowedTotalCommissionTypes: pluginConfig.referralConfig.allowedTotalCommissionTypes,
|
|
2163
|
+
cartAmountsInMinorUnits: cartMinor
|
|
1948
2164
|
});
|
|
1949
2165
|
const roundedPartnerCommission = roundTo2(result.partnerCommission);
|
|
1950
2166
|
const roundedCustomerDiscount = roundTo2(Math.max(0, result.customerDiscount));
|
|
@@ -1955,39 +2171,97 @@ const recalculateCartHook = (pluginConfig) => async ({ data, req, originalDoc })
|
|
|
1955
2171
|
}
|
|
1956
2172
|
}
|
|
1957
2173
|
} else if (readField$1(mutableData, fields.cartAppliedReferralCodeField) === null) clearReferralFields(mutableData, fields);
|
|
1958
|
-
const
|
|
1959
|
-
const canUseCouponWithReferral = !pluginConfig.enableReferrals || pluginConfig.referralConfig.allowBothSystems ||
|
|
1960
|
-
if (appliedCouponID != null && canUseCouponWithReferral) {
|
|
2174
|
+
const referralOnCart = appliedReferralID != null;
|
|
2175
|
+
const canUseCouponWithReferral = !pluginConfig.enableReferrals || pluginConfig.referralConfig.allowBothSystems || !referralOnCart;
|
|
2176
|
+
if (appliedCouponID != null && !canUseCouponWithReferral) {
|
|
2177
|
+
clearCouponFields(mutableData, fields);
|
|
2178
|
+
couponDiscount = 0;
|
|
2179
|
+
logCouponCartDebug("recalculateCart: cleared coupon (referral on cart with allowBothSystems=false)", {
|
|
2180
|
+
cartCurrency,
|
|
2181
|
+
cartMinor,
|
|
2182
|
+
calculatedSubtotal,
|
|
2183
|
+
subtotalMajor,
|
|
2184
|
+
appliedCouponID,
|
|
2185
|
+
referralOnCart
|
|
2186
|
+
}, req);
|
|
2187
|
+
} else if (appliedCouponID != null && canUseCouponWithReferral) {
|
|
1961
2188
|
const coupon = (await req.payload.find({
|
|
1962
2189
|
collection: pluginConfig.collections.couponsSlug,
|
|
1963
2190
|
where: { id: { equals: appliedCouponID } },
|
|
1964
2191
|
limit: 1
|
|
1965
2192
|
}))?.docs?.[0];
|
|
1966
|
-
if (!coupon)
|
|
1967
|
-
|
|
2193
|
+
if (!coupon) {
|
|
2194
|
+
clearCouponFields(mutableData, fields);
|
|
2195
|
+
logCouponCartDebug("recalculateCart: cleared coupon (document not found)", { appliedCouponID }, req);
|
|
2196
|
+
} else {
|
|
1968
2197
|
const now = /* @__PURE__ */ new Date();
|
|
1969
2198
|
const activeFrom = coupon.activeFrom ? new Date(coupon.activeFrom) : null;
|
|
1970
2199
|
const activeUntil = coupon.activeUntil ? new Date(coupon.activeUntil) : null;
|
|
1971
2200
|
const isValidDate = (!activeFrom || now >= activeFrom) && (!activeUntil || now <= activeUntil);
|
|
1972
2201
|
const underUsage = !coupon.usageLimit || Number(coupon.usageCount || 0) < Number(coupon.usageLimit || 0);
|
|
1973
|
-
if (!isValidDate || !underUsage)
|
|
1974
|
-
|
|
1975
|
-
|
|
2202
|
+
if (!isValidDate || !underUsage) {
|
|
2203
|
+
clearCouponFields(mutableData, fields);
|
|
2204
|
+
logCouponCartDebug("recalculateCart: cleared coupon (inactive or usage limit)", {
|
|
2205
|
+
appliedCouponID,
|
|
2206
|
+
isValidDate,
|
|
2207
|
+
underUsage
|
|
2208
|
+
}, req);
|
|
2209
|
+
} else {
|
|
2210
|
+
const discountMajor = calculateCouponDiscount({
|
|
1976
2211
|
coupon,
|
|
1977
|
-
cartTotal:
|
|
1978
|
-
})
|
|
2212
|
+
cartTotal: subtotalMajor
|
|
2213
|
+
});
|
|
2214
|
+
couponDiscount = cartMinor ? majorToMinor2dp(discountMajor) : roundTo2(discountMajor);
|
|
1979
2215
|
writeField(mutableData, fields.cartDiscountAmountField, couponDiscount);
|
|
2216
|
+
logCouponCartDebug("recalculateCart: coupon discount computed", {
|
|
2217
|
+
couponId: appliedCouponID,
|
|
2218
|
+
couponType: coupon.type,
|
|
2219
|
+
couponValue: coupon.value,
|
|
2220
|
+
cartCurrency,
|
|
2221
|
+
cartMinor,
|
|
2222
|
+
calculatedSubtotal,
|
|
2223
|
+
subtotalMajor,
|
|
2224
|
+
discountMajor,
|
|
2225
|
+
couponDiscountStored: couponDiscount
|
|
2226
|
+
}, req);
|
|
1980
2227
|
}
|
|
1981
2228
|
}
|
|
1982
2229
|
} else if (readField$1(mutableData, fields.cartAppliedCouponField) === null) {
|
|
1983
2230
|
clearCouponFields(mutableData, fields);
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
|
|
2231
|
+
if (!referralOnCart) {
|
|
2232
|
+
writeField(mutableData, fields.cartCustomerDiscountField, 0);
|
|
2233
|
+
writeField(mutableData, fields.cartPartnerCommissionField, 0);
|
|
2234
|
+
const st = Number(resolvers.getCartSubtotal(mutableData)) || calculatedSubtotal;
|
|
2235
|
+
writeField(mutableData, fields.cartTotalField, cartMinor ? Math.round(st) : roundTo2(st));
|
|
2236
|
+
logCouponCartDebug("recalculateCart: no applied coupon field (early exit)", {
|
|
2237
|
+
cartCurrency,
|
|
2238
|
+
cartMinor,
|
|
2239
|
+
calculatedSubtotal,
|
|
2240
|
+
totalSetTo: cartMinor ? Math.round(st) : roundTo2(st)
|
|
2241
|
+
}, req);
|
|
2242
|
+
return mutableData;
|
|
2243
|
+
}
|
|
2244
|
+
logCouponCartDebug("recalculateCart: no coupon; referral present, computing total with referral discount", {
|
|
2245
|
+
cartCurrency,
|
|
2246
|
+
cartMinor,
|
|
2247
|
+
calculatedSubtotal,
|
|
2248
|
+
referralOnCart
|
|
2249
|
+
}, req);
|
|
1988
2250
|
}
|
|
1989
|
-
const nextTotal = roundTo2(Math.max(0, calculatedSubtotal - customerDiscount - couponDiscount));
|
|
2251
|
+
const nextTotal = cartMinor ? Math.max(0, Math.round(calculatedSubtotal) - Math.round(customerDiscount) - Math.round(couponDiscount)) : roundTo2(Math.max(0, calculatedSubtotal - customerDiscount - couponDiscount));
|
|
1990
2252
|
writeField(mutableData, fields.cartTotalField, nextTotal);
|
|
2253
|
+
logCouponCartDebug("recalculateCart: final", {
|
|
2254
|
+
cartCurrency,
|
|
2255
|
+
cartMinor,
|
|
2256
|
+
calculatedSubtotal,
|
|
2257
|
+
subtotalMajor,
|
|
2258
|
+
customerDiscount,
|
|
2259
|
+
couponDiscount,
|
|
2260
|
+
nextTotal,
|
|
2261
|
+
appliedCouponIDAfter: relationId$3(readField$1(mutableData, fields.cartAppliedCouponField)),
|
|
2262
|
+
referralOnCart,
|
|
2263
|
+
canUseCouponWithReferral
|
|
2264
|
+
}, req);
|
|
1991
2265
|
return mutableData;
|
|
1992
2266
|
};
|
|
1993
2267
|
//#endregion
|
|
@@ -2235,7 +2509,8 @@ const sanitizePluginConfig = ({ pluginConfig }) => {
|
|
|
2235
2509
|
integration: {
|
|
2236
2510
|
collections: integrationCollections,
|
|
2237
2511
|
fields: integrationFields,
|
|
2238
|
-
resolvers: integrationResolvers
|
|
2512
|
+
resolvers: integrationResolvers,
|
|
2513
|
+
cartAmountsInMinorUnits: toBoolean(pluginConfig?.integration?.cartAmountsInMinorUnits, false)
|
|
2239
2514
|
},
|
|
2240
2515
|
referralConfig: {
|
|
2241
2516
|
allowBothSystems: pluginConfig?.referralConfig?.allowBothSystems ?? false,
|
|
@@ -2399,16 +2674,8 @@ const payloadEcommerceCouponPlugin = (pluginOptions = {}) => async (incomingConf
|
|
|
2399
2674
|
if (pluginOptions.collections?.referralProgramsCollectionOverride) referralProgramsCollection = await pluginOptions.collections.referralProgramsCollectionOverride({ defaultCollection: referralProgramsCollection });
|
|
2400
2675
|
if (pluginOptions.collections?.referralCodesCollectionOverride) referralCodesCollection = await pluginOptions.collections.referralCodesCollectionOverride({ defaultCollection: referralCodesCollection });
|
|
2401
2676
|
collectionsToAdd.push(referralProgramsCollection, referralCodesCollection);
|
|
2402
|
-
|
|
2403
|
-
|
|
2404
|
-
if (pluginOptions.collections?.couponsCollectionOverride) couponsCollection = await pluginOptions.collections.couponsCollectionOverride({ defaultCollection: couponsCollection });
|
|
2405
|
-
couponsCollection = ensureCouponCollectionEndpoints({
|
|
2406
|
-
collection: couponsCollection,
|
|
2407
|
-
pluginConfig
|
|
2408
|
-
});
|
|
2409
|
-
collectionsToAdd.push(couponsCollection);
|
|
2410
|
-
}
|
|
2411
|
-
} else {
|
|
2677
|
+
}
|
|
2678
|
+
{
|
|
2412
2679
|
let couponsCollection = createCouponsCollection(pluginConfig);
|
|
2413
2680
|
if (pluginOptions.collections?.couponsCollectionOverride) couponsCollection = await pluginOptions.collections.couponsCollectionOverride({ defaultCollection: couponsCollection });
|
|
2414
2681
|
couponsCollection = ensureCouponCollectionEndpoints({
|
|
@@ -2456,7 +2723,7 @@ const payloadEcommerceCouponPlugin = (pluginOptions = {}) => async (incomingConf
|
|
|
2456
2723
|
admin: { description: "Customer discount amount for this cart" }
|
|
2457
2724
|
}
|
|
2458
2725
|
];
|
|
2459
|
-
if (
|
|
2726
|
+
if (allSlugs.has(pluginConfig.collections.couponsSlug)) cartReferralFields.push({
|
|
2460
2727
|
name: cartAppliedCouponField,
|
|
2461
2728
|
type: "relationship",
|
|
2462
2729
|
relationTo: pluginConfig.collections.couponsSlug,
|
|
@@ -2494,7 +2761,7 @@ const payloadEcommerceCouponPlugin = (pluginOptions = {}) => async (incomingConf
|
|
|
2494
2761
|
}
|
|
2495
2762
|
}
|
|
2496
2763
|
];
|
|
2497
|
-
if (
|
|
2764
|
+
if (allSlugs.has(pluginConfig.collections.couponsSlug)) orderReferralFields.push({
|
|
2498
2765
|
name: orderAppliedCouponField,
|
|
2499
2766
|
type: "relationship",
|
|
2500
2767
|
relationTo: pluginConfig.collections.couponsSlug,
|
|
@@ -2591,7 +2858,7 @@ function resolveEndpoints(input) {
|
|
|
2591
2858
|
* @param endpointConfig - Optional endpoint override config
|
|
2592
2859
|
*/
|
|
2593
2860
|
async function useCouponCode(options, endpointConfig) {
|
|
2594
|
-
const { code, cartID, customerEmail } = options;
|
|
2861
|
+
const { code, cartID, customerEmail, secret } = options;
|
|
2595
2862
|
if (!code) return {
|
|
2596
2863
|
success: false,
|
|
2597
2864
|
message: "Coupon code is required",
|
|
@@ -2605,7 +2872,8 @@ async function useCouponCode(options, endpointConfig) {
|
|
|
2605
2872
|
body: JSON.stringify({
|
|
2606
2873
|
code,
|
|
2607
2874
|
cartID,
|
|
2608
|
-
customerEmail
|
|
2875
|
+
customerEmail,
|
|
2876
|
+
...typeof secret === "string" && secret.trim().length > 0 ? { secret: secret.trim() } : {}
|
|
2609
2877
|
})
|
|
2610
2878
|
});
|
|
2611
2879
|
const data = await response.json();
|