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