@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/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 = 10%. If fixed, interpreted in ${defaultCurrency} (smallest currency units)`,
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 amount in ${defaultCurrency} (smallest currency unit). Leave empty for no cap.` }
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 order value required in ${defaultCurrency} (smallest currency units)` }
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 order value allowed in ${defaultCurrency} (smallest currency units)` }
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$5(value) {
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$5).filter((v) => v != null);
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$5(item?.category ?? item?.product?.category);
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, minOrderAmountCents, allowedTotalCommissionTypes }) {
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 = minOrderAmountCents != null && Number.isFinite(minOrderAmountCents) ? minOrderAmountCents : typeof rule?.minOrderAmount === "number" && Number.isFinite(rule.minOrderAmount) ? toCents(rule.minOrderAmount) : null;
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$5(item.product);
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
- * All monetary inputs are in NORMAL CURRENCY.
977
- * Returns results in NORMAL CURRENCY (2 dp).
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
- const programMinOrderAmountCents = typeof program?.minOrderAmount === "number" && Number.isFinite(program.minOrderAmount) ? toCents(program.minOrderAmount) : null;
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: fromCents(totalPartnerCents),
1021
- customerDiscount: fromCents(totalCustomerCents)
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 relationId$4(value) {
1035
- if (value == null) return null;
1036
- if (typeof value === "string" || typeof value === "number") return value;
1037
- if (typeof value === "object" && (typeof value.id === "string" || typeof value.id === "number")) return value.id;
1038
- return null;
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$4(readField$3(cart, fields.cartAppliedCouponField));
1130
- const cartAppliedReferral = relationId$4(readField$3(cart, fields.cartAppliedReferralCodeField));
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$4(readField$3(cart, fields.cartAppliedCouponField)) === coupon.id) return Response.json({
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 cartSubtotal = Number(resolvers.getCartSubtotal(cart)) || 0;
1224
- const cartTotal = Number(resolvers.getCartTotal(cart)) || cartSubtotal || 0;
1225
- if (coupon.minOrderValue && cartSubtotal < coupon.minOrderValue) return Response.json({
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 && cartSubtotal > coupon.maxOrderValue) return Response.json({
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 discountAmount = calculateCouponDiscount({
1360
+ const discountMajor = calculateCouponDiscount({
1234
1361
  coupon,
1235
- cartTotal: cartSubtotal
1362
+ cartTotal: cartSubtotalMajor
1236
1363
  });
1237
- const nextTotal = roundTo2(Math.max(0, cartTotal - discountAmount));
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$4(readField$3(cart, fields.cartAppliedReferralCodeField)) === referralCode.id) return Response.json({
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" && cartSubtotal < minOrderAmount) return Response.json({
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$3(value) {
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$3(c?.id)).filter((id) => id != null);
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$3(readField$2(order, fields.orderAppliedReferralCodeField));
1437
- const matchedCode = referralCodes.find((c) => relationId$3(c?.id) === orderReferralID);
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$3(firstCode?.program);
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$2(value) {
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
- if (minOrderValue && cartValue < minOrderValue) return Response.json({
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 && cartValue > maxOrderValue) return Response.json({
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
- if (couponData.type === "percentage") {
1679
- discount = roundTo2(cartValue * couponData.value / 100);
1680
- if (couponData.maxDiscountAmount != null && discount > couponData.maxDiscountAmount) discount = roundTo2(couponData.maxDiscountAmount);
1681
- } else if (couponData.type === "fixed") {
1682
- discount = roundTo2(couponData.value);
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$2(referralData.program);
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 cartTotal = cart ? Number(resolvers.getCartSubtotal(cart)) || Number(resolvers.getCartTotal(cart)) || 0 : 0;
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" && cartTotal < minOrderAmount) return Response.json({
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 = cartTotal > 0 ? Math.min(customerDiscount, cartTotal) : customerDiscount;
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 ${roundedCustomerDiscount.toFixed(2)} discount with this referral code`
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 = readField$1(mutableData, fields.cartAppliedReferralCodeField) !== void 0 ? readField$1(mutableData, fields.cartAppliedReferralCodeField) : readField$1(original, fields.cartAppliedReferralCodeField);
1877
- const effectiveAppliedCoupon = readField$1(mutableData, fields.cartAppliedCouponField) !== void 0 ? readField$1(mutableData, fields.cartAppliedCouponField) : readField$1(original, fields.cartAppliedCouponField);
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$1(value);
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: pluginConfig.defaultCurrency
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
- writeField(mutableData, fields.cartSubtotalField, roundTo2(calculatedSubtotal));
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$1(effectiveAppliedReferral);
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$1(referralCode.program);
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" && calculatedSubtotal < minOrderAmount) clearReferralFields(mutableData, fields);
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: pluginConfig.defaultCurrency,
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 appliedCouponID = relationId$1(effectiveAppliedCoupon);
1959
- const canUseCouponWithReferral = !pluginConfig.enableReferrals || pluginConfig.referralConfig.allowBothSystems || relationId$1(readField$1(mutableData, fields.cartAppliedReferralCodeField)) == null;
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) clearCouponFields(mutableData, fields);
1967
- else {
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) clearCouponFields(mutableData, fields);
1974
- else {
1975
- couponDiscount = roundTo2(calculateCouponDiscount({
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: calculatedSubtotal
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
- writeField(mutableData, fields.cartCustomerDiscountField, 0);
1985
- writeField(mutableData, fields.cartPartnerCommissionField, 0);
1986
- writeField(mutableData, fields.cartTotalField, roundTo2(Number(resolvers.getCartSubtotal(mutableData)) || calculatedSubtotal));
1987
- return mutableData;
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
- if (pluginConfig.referralConfig.allowBothSystems) {
2403
- let couponsCollection = createCouponsCollection(pluginConfig);
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 (pluginConfig.referralConfig.allowBothSystems && allSlugs.has(pluginConfig.collections.couponsSlug)) cartReferralFields.push({
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 (pluginConfig.referralConfig.allowBothSystems && allSlugs.has(pluginConfig.collections.couponsSlug)) orderReferralFields.push({
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();