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