@wtree/payload-ecommerce-coupon 3.77.0 → 3.77.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -27,6 +27,8 @@ Production-ready coupon and referral system plugin for **Payload CMS** with seam
27
27
  - ✅ **Commission Rules** – **Required.** At least one rule per program. Each rule supports:
28
28
  - **Direct Basis**: Separate Reward (Partner) and Referee Reward (Customer).
29
29
  - **Shared Basis**: Define a total "pot" (e.g., 20% of order) and split it (e.g., 50/50) between partner and customer.
30
+ - ✅ **Rule-level Minimum Order** – Optional `minOrderAmount` per commission rule, validated on apply/validate/recalculate
31
+ - ✅ **Fixed-only Mode** – Restrict `totalCommission.type` to fixed only via `referralConfig.allowedTotalCommissionTypes: ['fixed']`
30
32
  - ✅ **Referrer/Referee inside each rule** – Partner gets commission, customer gets discount; type (percentage/fixed), value, and optional max cap per rule.
31
33
  - ✅ **Partner Tracking** – Commission earnings and referral performance (credited when order is placed)
32
34
  - ✅ **Auto-Generated Codes** – Unique referral codes for each partner
@@ -85,6 +87,8 @@ export default buildConfig({
85
87
  singleCodePerCart: true, // Only one code per order
86
88
  defaultPartnerSplit: 70, // 70% to partner
87
89
  defaultCustomerSplit: 30, // 30% discount to customer
90
+ // Set fixed-only mode for production partner programs
91
+ allowedTotalCommissionTypes: ["fixed"], // Default: ["fixed", "percentage"]
88
92
  },
89
93
 
90
94
  // Custom admin panel groups
@@ -110,6 +114,13 @@ export default buildConfig({
110
114
  isPartner: ({ req }) => req.user?.role === "partner",
111
115
  },
112
116
 
117
+ // Optional: role resolver for custom user schemas
118
+ // roleConfig: {
119
+ // roleFieldPaths: ["role", "roles", "account.roles"],
120
+ // adminRoleValues: ["admin"],
121
+ // partnerRoleValues: ["partner", "affiliate"],
122
+ // },
123
+
113
124
  // Optional: for per-customer coupon limit (defaults shown)
114
125
  // orderIntegration: {
115
126
  // ordersSlug: 'orders',
@@ -138,7 +149,7 @@ This will create collections for:
138
149
 
139
150
  ### 3. Setting Up Partner Role
140
151
 
141
- To enable the partner dashboard and role-based access, add a `role` field to your Users collection:
152
+ To enable the partner dashboard and role-based access, add a `role` or `roles` field to your Users collection (or configure custom paths with `roleConfig`):
142
153
 
143
154
  ```typescript
144
155
  // collections/Users.ts
@@ -175,6 +186,16 @@ export const Users: CollectionConfig = {
175
186
  };
176
187
  ```
177
188
 
189
+ If your user schema stores roles in a custom structure, configure:
190
+
191
+ ```typescript
192
+ roleConfig: {
193
+ roleFieldPaths: ["role", "roles", "profile.accountRoles"],
194
+ adminRoleValues: ["admin"],
195
+ partnerRoleValues: ["partner", "affiliate"],
196
+ }
197
+ ```
198
+
178
199
  ### 4. Record Usage When Order Is Placed
179
200
 
180
201
  Coupon and referral **usage is not counted when a code is applied** to the cart. It is counted only when an **order is placed successfully** (e.g. paid). You must call the plugin when converting cart to order:
@@ -1 +1 @@
1
- {"version":3,"file":"createReferralCodesCollection.d.ts","sourceRoot":"","sources":["../../src/collections/createReferralCodesCollection.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,SAAS,CAAA;AAE/C,OAAO,KAAK,EAAE,4BAA4B,EAAE,MAAM,UAAU,CAAA;AAE5D,eAAO,MAAM,6BAA6B,GACxC,cAAc,4BAA4B,KACzC,gBA+LF,CAAA"}
1
+ {"version":3,"file":"createReferralCodesCollection.d.ts","sourceRoot":"","sources":["../../src/collections/createReferralCodesCollection.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,SAAS,CAAA;AAE/C,OAAO,KAAK,EAAE,4BAA4B,EAAE,MAAM,UAAU,CAAA;AAG5D,eAAO,MAAM,6BAA6B,GACxC,cAAc,4BAA4B,KACzC,gBA0LF,CAAA"}
@@ -1 +1 @@
1
- {"version":3,"file":"createReferralProgramsCollection.d.ts","sourceRoot":"","sources":["../../src/collections/createReferralProgramsCollection.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,SAAS,CAAA;AAE/C,OAAO,KAAK,EAAE,4BAA4B,EAAE,MAAM,UAAU,CAAA;AAwB5D,eAAO,MAAM,gCAAgC,GAC3C,cAAc,4BAA4B,KACzC,gBA6PF,CAAA"}
1
+ {"version":3,"file":"createReferralProgramsCollection.d.ts","sourceRoot":"","sources":["../../src/collections/createReferralProgramsCollection.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,SAAS,CAAA;AAE/C,OAAO,KAAK,EAAE,4BAA4B,EAAE,MAAM,UAAU,CAAA;AAyB5D,eAAO,MAAM,gCAAgC,GAC3C,cAAc,4BAA4B,KACzC,gBAiRF,CAAA"}
@@ -1 +1 @@
1
- {"version":3,"file":"applyCoupon.d.ts","sourceRoot":"","sources":["../../src/endpoints/applyCoupon.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,cAAc,EAAE,MAAM,SAAS,CAAA;AACvD,OAAO,KAAK,EAAE,4BAA4B,EAAE,MAAM,UAAU,CAAA;AAO5D,KAAK,IAAI,GAAG;IACV,YAAY,EAAE,4BAA4B,CAAA;CAC3C,CAAA;AAcD,eAAO,MAAM,kBAAkB,GAC5B,kBAAkB,IAAI,KAAG,cAyFzB,CAAA;AA4SH,eAAO,MAAM,mBAAmB,GAAI,kBAAkB,IAAI,KAAG,QAI3D,CAAA"}
1
+ {"version":3,"file":"applyCoupon.d.ts","sourceRoot":"","sources":["../../src/endpoints/applyCoupon.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,cAAc,EAAE,MAAM,SAAS,CAAA;AACvD,OAAO,KAAK,EAAE,4BAA4B,EAAE,MAAM,UAAU,CAAA;AAQ5D,KAAK,IAAI,GAAG;IACV,YAAY,EAAE,4BAA4B,CAAA;CAC3C,CAAA;AAcD,eAAO,MAAM,kBAAkB,GAC5B,kBAAkB,IAAI,KAAG,cAyFzB,CAAA;AA4TH,eAAO,MAAM,mBAAmB,GAAI,kBAAkB,IAAI,KAAG,QAI3D,CAAA"}
@@ -1 +1 @@
1
- {"version":3,"file":"partnerStats.d.ts","sourceRoot":"","sources":["../../src/endpoints/partnerStats.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,cAAc,EAAE,MAAM,SAAS,CAAA;AAEvD,OAAO,KAAK,EAAsC,4BAA4B,EAAE,MAAM,UAAU,CAAA;AAEhG,KAAK,IAAI,GAAG;IACV,YAAY,EAAE,4BAA4B,CAAA;CAC3C,CAAA;AAED,eAAO,MAAM,mBAAmB,GAC7B,kBAAkB,IAAI,KAAG,cA8KzB,CAAA;AAEH,eAAO,MAAM,oBAAoB,GAAI,kBAAkB,IAAI,KAAG,QAI5D,CAAA"}
1
+ {"version":3,"file":"partnerStats.d.ts","sourceRoot":"","sources":["../../src/endpoints/partnerStats.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,cAAc,EAAE,MAAM,SAAS,CAAA;AAEvD,OAAO,KAAK,EAAsC,4BAA4B,EAAE,MAAM,UAAU,CAAA;AAGhG,KAAK,IAAI,GAAG;IACV,YAAY,EAAE,4BAA4B,CAAA;CAC3C,CAAA;AAED,eAAO,MAAM,mBAAmB,GAC7B,kBAAkB,IAAI,KAAG,cA6KzB,CAAA;AAEH,eAAO,MAAM,oBAAoB,GAAI,kBAAkB,IAAI,KAAG,QAI5D,CAAA"}
@@ -1 +1 @@
1
- {"version":3,"file":"validateCoupon.d.ts","sourceRoot":"","sources":["../../src/endpoints/validateCoupon.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,cAAc,EAAE,MAAM,SAAS,CAAA;AAEvD,OAAO,KAAK,EAAE,4BAA4B,EAAE,MAAM,UAAU,CAAA;AAI5D,KAAK,IAAI,GAAG;IACV,YAAY,EAAE,4BAA4B,CAAA;CAC3C,CAAA;AAED,eAAO,MAAM,qBAAqB,GAC/B,kBAAkB,IAAI,KAAG,cAkCzB,CAAA;AA2QH,eAAO,MAAM,sBAAsB,GAAI,kBAAkB,IAAI,KAAG,QAI9D,CAAA"}
1
+ {"version":3,"file":"validateCoupon.d.ts","sourceRoot":"","sources":["../../src/endpoints/validateCoupon.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,cAAc,EAAE,MAAM,SAAS,CAAA;AAEvD,OAAO,KAAK,EAAE,4BAA4B,EAAE,MAAM,UAAU,CAAA;AAO5D,KAAK,IAAI,GAAG;IACV,YAAY,EAAE,4BAA4B,CAAA;CAC3C,CAAA;AAED,eAAO,MAAM,qBAAqB,GAC/B,kBAAkB,IAAI,KAAG,cAkCzB,CAAA;AA4RH,eAAO,MAAM,sBAAsB,GAAI,kBAAkB,IAAI,KAAG,QAI9D,CAAA"}
@@ -1 +1 @@
1
- {"version":3,"file":"recalculateCart.d.ts","sourceRoot":"","sources":["../../src/hooks/recalculateCart.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,0BAA0B,EAAE,MAAM,SAAS,CAAA;AACzD,OAAO,KAAK,EAAE,4BAA4B,EAAE,MAAM,UAAU,CAAA;AAQ5D,eAAO,MAAM,mBAAmB,GAC7B,cAAc,4BAA4B,KAAG,0BAiO7C,CAAA"}
1
+ {"version":3,"file":"recalculateCart.d.ts","sourceRoot":"","sources":["../../src/hooks/recalculateCart.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,0BAA0B,EAAE,MAAM,SAAS,CAAA;AACzD,OAAO,KAAK,EAAE,4BAA4B,EAAE,MAAM,UAAU,CAAA;AAS5D,eAAO,MAAM,mBAAmB,GAC7B,cAAc,4BAA4B,KAAG,0BA2N7C,CAAA"}
package/dist/index.d.ts CHANGED
@@ -3,10 +3,10 @@ import { createReferralCodesCollection } from './collections/createReferralCodes
3
3
  import { createReferralProgramsCollection } from './collections/createReferralProgramsCollection';
4
4
  import { payloadEcommerceCouponPlugin } from './plugin';
5
5
  export { useCouponCode, usePartnerStats, validateCouponCode } from './client/hooks';
6
- export { calculateCommissionAndDiscount } from './utilities/calculateValues';
6
+ export { calculateCommissionAndDiscount, getProgramMinimumOrderAmount, } from './utilities/calculateValues';
7
7
  export { getCartTotalWithDiscounts } from './utilities/getCartTotalWithDiscounts';
8
8
  export { recordCouponUsageForOrder } from './utilities/recordCouponUsageForOrder';
9
9
  export { createCouponsCollection, createReferralCodesCollection, createReferralProgramsCollection, payloadEcommerceCouponPlugin as payloadEcommerceCoupon, };
10
- export type { AdminGroupConfig, ApplyCouponHook, ApplyCouponResponse, CouponPluginAccess, CouponPluginCollections, CouponPluginOptions, OrderIntegrationConfig, PartnerDashboardConfig, PartnerDashboardData, PartnerStats, ReferralProgramConfig, } from './types';
10
+ export type { AdminGroupConfig, ApplyCouponHook, ApplyCouponResponse, CouponPluginAccess, CouponPluginCollections, CouponPluginOptions, OrderIntegrationConfig, PartnerDashboardConfig, PartnerDashboardData, PartnerStats, ReferralProgramConfig, RoleConfig, } from './types';
11
11
  export type { CartLike } from './utilities/getCartTotalWithDiscounts';
12
12
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,uBAAuB,EAAE,MAAM,uCAAuC,CAAA;AAC/E,OAAO,EAAE,6BAA6B,EAAE,MAAM,6CAA6C,CAAA;AAC3F,OAAO,EAAE,gCAAgC,EAAE,MAAM,gDAAgD,CAAA;AACjG,OAAO,EAAE,4BAA4B,EAAE,MAAM,UAAU,CAAA;AAEvD,OAAO,EAAE,aAAa,EAAE,eAAe,EAAE,kBAAkB,EAAE,MAAM,gBAAgB,CAAA;AACnF,OAAO,EAAE,8BAA8B,EAAE,MAAM,6BAA6B,CAAA;AAC5E,OAAO,EAAE,yBAAyB,EAAE,MAAM,uCAAuC,CAAA;AACjF,OAAO,EAAE,yBAAyB,EAAE,MAAM,uCAAuC,CAAA;AACjF,OAAO,EACL,uBAAuB,EACvB,6BAA6B,EAC7B,gCAAgC,EAChC,4BAA4B,IAAI,sBAAsB,GACvD,CAAA;AAED,YAAY,EACV,gBAAgB,EAChB,eAAe,EACf,mBAAmB,EACnB,kBAAkB,EAClB,uBAAuB,EACvB,mBAAmB,EACnB,sBAAsB,EACtB,sBAAsB,EACtB,oBAAoB,EACpB,YAAY,EACZ,qBAAqB,GACtB,MAAM,SAAS,CAAA;AAChB,YAAY,EAAE,QAAQ,EAAE,MAAM,uCAAuC,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,uBAAuB,EAAE,MAAM,uCAAuC,CAAA;AAC/E,OAAO,EAAE,6BAA6B,EAAE,MAAM,6CAA6C,CAAA;AAC3F,OAAO,EAAE,gCAAgC,EAAE,MAAM,gDAAgD,CAAA;AACjG,OAAO,EAAE,4BAA4B,EAAE,MAAM,UAAU,CAAA;AAEvD,OAAO,EAAE,aAAa,EAAE,eAAe,EAAE,kBAAkB,EAAE,MAAM,gBAAgB,CAAA;AACnF,OAAO,EACL,8BAA8B,EAC9B,4BAA4B,GAC7B,MAAM,6BAA6B,CAAA;AACpC,OAAO,EAAE,yBAAyB,EAAE,MAAM,uCAAuC,CAAA;AACjF,OAAO,EAAE,yBAAyB,EAAE,MAAM,uCAAuC,CAAA;AACjF,OAAO,EACL,uBAAuB,EACvB,6BAA6B,EAC7B,gCAAgC,EAChC,4BAA4B,IAAI,sBAAsB,GACvD,CAAA;AAED,YAAY,EACV,gBAAgB,EAChB,eAAe,EACf,mBAAmB,EACnB,kBAAkB,EAClB,uBAAuB,EACvB,mBAAmB,EACnB,sBAAsB,EACtB,sBAAsB,EACtB,oBAAoB,EACpB,YAAY,EACZ,qBAAqB,EACrB,UAAU,GACX,MAAM,SAAS,CAAA;AAChB,YAAY,EAAE,QAAQ,EAAE,MAAM,uCAAuC,CAAA"}
package/dist/index.js CHANGED
@@ -120,10 +120,59 @@ const createCouponsCollection = (pluginConfig) => {
120
120
  };
121
121
  };
122
122
 
123
+ //#endregion
124
+ //#region src/utilities/userRoles.ts
125
+ function readByPath(input, path) {
126
+ if (!input || typeof input !== "object" || !path) return void 0;
127
+ return path.split(".").reduce((acc, key) => {
128
+ if (!acc || typeof acc !== "object") return void 0;
129
+ return acc[key];
130
+ }, input);
131
+ }
132
+ function toRoleArray(value) {
133
+ if (typeof value === "string" && value.trim().length > 0) return [value];
134
+ if (Array.isArray(value)) return value.filter((item) => typeof item === "string").map((item) => item.trim()).filter(Boolean);
135
+ return [];
136
+ }
137
+ const normalizeRoleValue = (value) => value.trim().toLowerCase();
138
+ const resolveUserRoles = ({ user, roleConfig }) => {
139
+ if (!user) return [];
140
+ if (typeof roleConfig.customRoleResolver === "function") {
141
+ const custom = roleConfig.customRoleResolver(user);
142
+ return Array.isArray(custom) ? custom.map(normalizeRoleValue).filter(Boolean) : [];
143
+ }
144
+ const roles = roleConfig.roleFieldPaths.flatMap((path) => toRoleArray(readByPath(user, path)));
145
+ return [...new Set(roles.map(normalizeRoleValue).filter(Boolean))];
146
+ };
147
+ const userHasAnyRole = ({ user, roleConfig, targetRoles }) => {
148
+ const userRoles = new Set(resolveUserRoles({
149
+ user,
150
+ roleConfig
151
+ }));
152
+ for (const target of targetRoles) if (userRoles.has(normalizeRoleValue(target))) return true;
153
+ return false;
154
+ };
155
+ const isPartnerUser = ({ user, roleConfig }) => userHasAnyRole({
156
+ user,
157
+ roleConfig,
158
+ targetRoles: roleConfig.partnerRoleValues
159
+ });
160
+ const isAdminUser = ({ user, roleConfig }) => userHasAnyRole({
161
+ user,
162
+ roleConfig,
163
+ targetRoles: roleConfig.adminRoleValues
164
+ });
165
+ const buildPartnerUserFilterWhere = ({ roleConfig }) => {
166
+ if (!roleConfig.roleFieldPaths.length || !roleConfig.partnerRoleValues.length) return true;
167
+ const conditions = roleConfig.roleFieldPaths.map((fieldPath) => ({ [fieldPath]: { in: roleConfig.partnerRoleValues } }));
168
+ if (conditions.length === 1) return conditions[0];
169
+ return { or: conditions };
170
+ };
171
+
123
172
  //#endregion
124
173
  //#region src/collections/createReferralCodesCollection.ts
125
174
  const createReferralCodesCollection = (pluginConfig) => {
126
- const { collections, access, adminGroups, defaultCurrency } = pluginConfig;
175
+ const { collections, access, adminGroups, defaultCurrency, roleConfig } = pluginConfig;
127
176
  return {
128
177
  slug: collections.referralCodesSlug,
129
178
  admin: {
@@ -141,15 +190,27 @@ const createReferralCodesCollection = (pluginConfig) => {
141
190
  read: ({ req }) => {
142
191
  const user = req?.user;
143
192
  if (!user) return false;
144
- if (user.role === "admin" || Array.isArray(user.roles) && user.roles.includes("admin")) return true;
145
- if (user.role === "partner" || Array.isArray(user.roles) && user.roles.includes("partner")) return { partner: { equals: user.id } };
193
+ if (isAdminUser({
194
+ user,
195
+ roleConfig
196
+ }) || access.isAdmin?.({ req })) return true;
197
+ if (isPartnerUser({
198
+ user,
199
+ roleConfig
200
+ }) || access.isPartner?.({ req })) return { partner: { equals: user.id } };
146
201
  return access.canUseReferrals ? access.canUseReferrals({ req }) : false;
147
202
  },
148
203
  create: ({ req }) => {
149
204
  const user = req?.user;
150
205
  if (!user) return false;
151
- if (user.role === "admin" || Array.isArray(user.roles) && user.roles.includes("admin")) return true;
152
- if (user.role === "partner" || Array.isArray(user.roles) && user.roles.includes("partner")) return true;
206
+ if (isAdminUser({
207
+ user,
208
+ roleConfig
209
+ }) || access.isAdmin?.({ req })) return true;
210
+ if (isPartnerUser({
211
+ user,
212
+ roleConfig
213
+ }) || access.isPartner?.({ req })) return true;
153
214
  return access.isAdmin ? access.isAdmin({ req }) : false;
154
215
  },
155
216
  update: access.isAdmin || (() => false),
@@ -175,7 +236,13 @@ const createReferralCodesCollection = (pluginConfig) => {
175
236
  type: "relationship",
176
237
  relationTo: "users",
177
238
  required: true,
178
- filterOptions: { roles: { contains: "partner" } },
239
+ filterOptions: ({ req, user }) => {
240
+ if (isAdminUser({
241
+ user: user || req?.user,
242
+ roleConfig
243
+ }) || access.isAdmin?.({ req })) return true;
244
+ return buildPartnerUserFilterWhere({ roleConfig });
245
+ },
179
246
  admin: { description: "The partner who owns this referral code" }
180
247
  },
181
248
  {
@@ -252,7 +319,10 @@ const createReferralCodesCollection = (pluginConfig) => {
252
319
  if (operation === "create" && !data.code && data.partner) data.code = `REF-${Date.now().toString(36)}-${Math.random().toString(36).substring(2, 8)}`.toUpperCase();
253
320
  if (operation === "create" && req.user) {
254
321
  const user = req.user;
255
- if (user.role === "partner" || Array.isArray(user.roles) && user.roles.includes("partner")) data.partner = user.id;
322
+ if (isPartnerUser({
323
+ user,
324
+ roleConfig
325
+ })) data.partner = user.id;
256
326
  }
257
327
  return data;
258
328
  }] },
@@ -274,6 +344,7 @@ const deriveCustomerSplit = (partnerSplit) => {
274
344
  };
275
345
  const createReferralProgramsCollection = (pluginConfig) => {
276
346
  const { collections, access, defaultCurrency, adminGroups, referralConfig } = pluginConfig;
347
+ const allowedTotalCommissionTypes = referralConfig.allowedTotalCommissionTypes;
277
348
  return {
278
349
  slug: collections.referralProgramsSlug,
279
350
  admin: {
@@ -296,7 +367,7 @@ const createReferralProgramsCollection = (pluginConfig) => {
296
367
  data.commissionRules = data.commissionRules.map((rule, index) => {
297
368
  const r = rule;
298
369
  if (!r.totalCommission) throw new Error(`Commission rule ${index + 1}: Total Commission is required`);
299
- if (!r.totalCommission.type || !["fixed", "percentage"].includes(r.totalCommission.type)) throw new Error(`Commission rule ${index + 1}: Total Commission type must be fixed or percentage`);
370
+ if (!r.totalCommission.type || !allowedTotalCommissionTypes.includes(r.totalCommission.type)) throw new Error(`Commission rule ${index + 1}: Total Commission type must be one of ${allowedTotalCommissionTypes.join(", ")}`);
300
371
  const totalValue = toNumber(r.totalCommission.value);
301
372
  if (totalValue == null || totalValue < 0) throw new Error(`Commission rule ${index + 1}: Total Commission value must be a non-negative number`);
302
373
  if (r.totalCommission.type === "percentage" && totalValue > 100) throw new Error(`Commission rule ${index + 1}: Percentage Total Commission cannot exceed 100`);
@@ -308,6 +379,8 @@ const createReferralProgramsCollection = (pluginConfig) => {
308
379
  const partnerSplit = toNumber(r.partnerSplit);
309
380
  if (partnerSplit == null || partnerSplit < 0 || partnerSplit > 100) throw new Error(`Commission rule ${index + 1}: Partner Split must be between 0 and 100`);
310
381
  const customerSplit = 100 - partnerSplit;
382
+ const minOrderAmount = toNumber(r.minOrderAmount);
383
+ if (minOrderAmount != null && minOrderAmount < 0) throw new Error(`Commission rule ${index + 1}: Minimum Order Amount must be a non-negative number`);
311
384
  return {
312
385
  ...rule,
313
386
  appliesTo: appliesTo === "categories" ? "segments" : appliesTo,
@@ -317,7 +390,8 @@ const createReferralProgramsCollection = (pluginConfig) => {
317
390
  maxAmount: maxAmount ?? null
318
391
  },
319
392
  partnerSplit,
320
- customerSplit
393
+ customerSplit,
394
+ minOrderAmount: minOrderAmount ?? null
321
395
  };
322
396
  });
323
397
  return data;
@@ -407,14 +481,11 @@ const createReferralProgramsCollection = (pluginConfig) => {
407
481
  name: "type",
408
482
  type: "select",
409
483
  required: true,
410
- options: [{
411
- label: "Fixed Amount",
412
- value: "fixed"
413
- }, {
414
- label: "Percentage of Order",
415
- value: "percentage"
416
- }],
417
- defaultValue: "percentage"
484
+ options: allowedTotalCommissionTypes.map((value) => ({
485
+ label: value === "fixed" ? "Fixed Amount" : "Percentage of Order",
486
+ value
487
+ })),
488
+ defaultValue: allowedTotalCommissionTypes.includes("fixed") ? "fixed" : "percentage"
418
489
  },
419
490
  {
420
491
  name: "value",
@@ -440,6 +511,12 @@ const createReferralProgramsCollection = (pluginConfig) => {
440
511
  defaultValue: referralConfig.defaultPartnerSplit,
441
512
  admin: { description: "Percentage of total commission given to Partner (0-100)" }
442
513
  },
514
+ {
515
+ name: "minOrderAmount",
516
+ type: "number",
517
+ min: 0,
518
+ admin: { description: `Minimum cart subtotal required for this rule in ${defaultCurrency}. Leave empty for no minimum.` }
519
+ },
443
520
  {
444
521
  name: "customerSplit",
445
522
  type: "number",
@@ -526,6 +603,7 @@ function relationId(value) {
526
603
  if (typeof value === "object" && (typeof value.id === "string" || typeof value.id === "number")) return value.id;
527
604
  return null;
528
605
  }
606
+ const allowedCommissionTypesSet = (allowed) => new Set((allowed && allowed.length ? allowed : ["fixed", "percentage"]).map((v) => v));
529
607
  function normalizeIds(values) {
530
608
  if (!Array.isArray(values)) return [];
531
609
  return values.map(relationId).filter((v) => v != null);
@@ -538,8 +616,10 @@ function getRuleSplits(rule) {
538
616
  customerSplit: typeof rule.customerSplit === "number" ? rule.customerSplit : typeof rule.refereeSplit === "number" ? rule.refereeSplit : 100 - partnerRaw
539
617
  };
540
618
  }
541
- function calculateItemRewardByRule({ rule, itemTotal, quantity }) {
619
+ function calculateItemRewardByRule({ rule, itemTotal, quantity, allowedTotalCommissionTypes }) {
620
+ const allowedTypes = allowedCommissionTypesSet(allowedTotalCommissionTypes);
542
621
  if (rule.totalCommission) {
622
+ if (!allowedTypes.has(rule.totalCommission.type)) return null;
543
623
  const splits = getRuleSplits(rule);
544
624
  if (!splits) return null;
545
625
  let totalPot = 0;
@@ -578,21 +658,27 @@ function getItemCategoryIds(item) {
578
658
  function getItemTagIds(item) {
579
659
  return Array.isArray(item?.product?.tags) ? normalizeIds(item.product.tags) : [];
580
660
  }
581
- function selectBestRuleForItem({ rules, item, itemTotal, quantity }) {
661
+ function selectBestRuleForItem({ rules, item, itemTotal, quantity, cartTotal, allowedTotalCommissionTypes }) {
662
+ const allowedTypes = allowedCommissionTypesSet(allowedTotalCommissionTypes);
663
+ const eligibleRules = rules.filter((rule) => {
664
+ if (!(rule?.totalCommission?.type ? allowedTypes.has(rule.totalCommission.type) : true)) return false;
665
+ if (typeof rule?.minOrderAmount === "number" && Number.isFinite(rule.minOrderAmount)) return cartTotal >= rule.minOrderAmount;
666
+ return true;
667
+ });
582
668
  const productId = relationId(item.product);
583
669
  const itemCategoryIds = new Set(getItemCategoryIds(item));
584
670
  const itemTagIds = new Set(getItemTagIds(item));
585
671
  const candidates = [
586
- rules.filter((r) => r.appliesTo === "products" && normalizeIds(r.products).some((id) => productId != null && id === productId)),
587
- rules.filter((r) => {
672
+ eligibleRules.filter((r) => r.appliesTo === "products" && normalizeIds(r.products).some((id) => productId != null && id === productId)),
673
+ eligibleRules.filter((r) => {
588
674
  if (!(r.appliesTo === "segments" || r.appliesTo === "categories")) return false;
589
675
  return normalizeIds(r.categories).some((id) => itemCategoryIds.has(id));
590
676
  }),
591
- rules.filter((r) => {
677
+ eligibleRules.filter((r) => {
592
678
  if (r.appliesTo !== "segments") return false;
593
679
  return normalizeIds(r.tags).some((id) => itemTagIds.has(id));
594
680
  }),
595
- rules.filter((r) => r.appliesTo === "all")
681
+ eligibleRules.filter((r) => r.appliesTo === "all")
596
682
  ].find((level) => level.length > 0) ?? [];
597
683
  if (!candidates.length) return null;
598
684
  let best = null;
@@ -600,7 +686,8 @@ function selectBestRuleForItem({ rules, item, itemTotal, quantity }) {
600
686
  const reward = calculateItemRewardByRule({
601
687
  rule,
602
688
  itemTotal,
603
- quantity
689
+ quantity,
690
+ allowedTotalCommissionTypes
604
691
  });
605
692
  if (!reward) continue;
606
693
  if (!best) {
@@ -624,7 +711,18 @@ function selectBestRuleForItem({ rules, item, itemTotal, quantity }) {
624
711
  }
625
712
  return best;
626
713
  }
627
- function calculateCommissionAndDiscount({ cartItems, program, currencyCode = "AED" }) {
714
+ function getProgramMinimumOrderAmount({ program, allowedTotalCommissionTypes }) {
715
+ const rules = Array.isArray(program?.commissionRules) ? program.commissionRules : [];
716
+ if (!rules.length) return null;
717
+ const allowedTypes = allowedCommissionTypesSet(allowedTotalCommissionTypes);
718
+ const minValues = rules.filter((rule) => {
719
+ if (rule?.totalCommission?.type) return allowedTypes.has(rule.totalCommission.type);
720
+ return true;
721
+ }).map((rule) => rule?.minOrderAmount).filter((value) => typeof value === "number" && Number.isFinite(value));
722
+ if (!minValues.length) return null;
723
+ return Math.min(...minValues);
724
+ }
725
+ function calculateCommissionAndDiscount({ cartItems, program, currencyCode = "AED", cartTotal = 0, allowedTotalCommissionTypes }) {
628
726
  const rules = Array.isArray(program?.commissionRules) ? program.commissionRules : [];
629
727
  if (!rules.length) return {
630
728
  partnerCommission: 0,
@@ -649,7 +747,9 @@ function calculateCommissionAndDiscount({ cartItems, program, currencyCode = "AE
649
747
  product
650
748
  },
651
749
  itemTotal,
652
- quantity
750
+ quantity,
751
+ cartTotal,
752
+ allowedTotalCommissionTypes
653
753
  });
654
754
  if (!bestMatch) continue;
655
755
  totalPartnerCommission += bestMatch.reward.partner;
@@ -877,10 +977,20 @@ async function handleReferralCode({ payload, code, cartID, cart, customerEmail:
877
977
  error: "Referral code already applied to this cart"
878
978
  }, { status: 400 });
879
979
  const cartTotal = cart.subtotal || cart.total || 0;
980
+ const minOrderAmount = getProgramMinimumOrderAmount({
981
+ program,
982
+ allowedTotalCommissionTypes: pluginConfig.referralConfig.allowedTotalCommissionTypes
983
+ });
984
+ if (typeof minOrderAmount === "number" && cartTotal < minOrderAmount) return Response.json({
985
+ success: false,
986
+ error: `Minimum order value of ${minOrderAmount} ${pluginConfig.defaultCurrency} required for this referral program`
987
+ }, { status: 400 });
880
988
  const { partnerCommission, customerDiscount } = calculateCommissionAndDiscount({
881
989
  cartItems: cart.items || [],
882
990
  program,
883
- currencyCode: pluginConfig.defaultCurrency
991
+ currencyCode: pluginConfig.defaultCurrency,
992
+ cartTotal,
993
+ allowedTotalCommissionTypes: pluginConfig.referralConfig.allowedTotalCommissionTypes
884
994
  });
885
995
  const roundedPartnerCommission = roundTo2(partnerCommission);
886
996
  const roundedCustomerDiscount = roundTo2(customerDiscount);
@@ -920,8 +1030,14 @@ const partnerStatsHandler = ({ pluginConfig }) => async (req) => {
920
1030
  error: "Authentication required"
921
1031
  }, { status: 401 });
922
1032
  const typedUser = user;
923
- const isPartner = typedUser.role === "partner" || Array.isArray(typedUser.roles) && typedUser.roles.includes("partner");
924
- const isAdmin = typedUser.role === "admin" || Array.isArray(typedUser.roles) && typedUser.roles.includes("admin");
1033
+ const isPartner = isPartnerUser({
1034
+ user: typedUser,
1035
+ roleConfig: pluginConfig.roleConfig
1036
+ }) || pluginConfig.access.isPartner?.({ req });
1037
+ const isAdmin = isAdminUser({
1038
+ user: typedUser,
1039
+ roleConfig: pluginConfig.roleConfig
1040
+ }) || pluginConfig.access.isAdmin?.({ req });
925
1041
  if (!isPartner && !isAdmin) return Response.json({
926
1042
  success: false,
927
1043
  error: "Partner access required"
@@ -1201,12 +1317,22 @@ async function validateReferralCode({ payload, code, cartID, pluginConfig }) {
1201
1317
  id: cartID,
1202
1318
  depth: 2
1203
1319
  }) : null;
1320
+ const cartTotal = cart ? cart.subtotal || cart.total || 0 : 0;
1321
+ const minOrderAmount = getProgramMinimumOrderAmount({
1322
+ program,
1323
+ allowedTotalCommissionTypes: pluginConfig.referralConfig.allowedTotalCommissionTypes
1324
+ });
1325
+ if (typeof minOrderAmount === "number" && cartTotal < minOrderAmount) return Response.json({
1326
+ success: false,
1327
+ error: `Minimum order value of ${minOrderAmount} ${pluginConfig.defaultCurrency} required for this referral program`
1328
+ }, { status: 400 });
1204
1329
  const { partnerCommission, customerDiscount } = calculateCommissionAndDiscount({
1205
1330
  cartItems: cart?.items || [],
1206
1331
  program,
1207
- currencyCode: pluginConfig.defaultCurrency
1332
+ currencyCode: pluginConfig.defaultCurrency,
1333
+ cartTotal,
1334
+ allowedTotalCommissionTypes: pluginConfig.referralConfig.allowedTotalCommissionTypes
1208
1335
  });
1209
- const cartTotal = cart ? cart.subtotal || cart.total || 0 : 0;
1210
1336
  const cappedCustomerDiscount = cartTotal > 0 ? Math.min(customerDiscount, cartTotal) : customerDiscount;
1211
1337
  const roundedPartnerCommission = roundTo2(partnerCommission);
1212
1338
  const roundedCustomerDiscount = roundTo2(cappedCustomerDiscount);
@@ -1232,12 +1358,6 @@ const validateCouponEndpoint = ({ pluginConfig }) => ({
1232
1358
  const recalculateCartHook = (pluginConfig) => async ({ data, req, originalDoc }) => {
1233
1359
  if (!req.payload) return data;
1234
1360
  const effectiveItems = data.items || originalDoc?.items || [];
1235
- console.log("[RecalculateCart] Hook triggered", {
1236
- hasDataItems: !!data.items,
1237
- dataItemsCount: data.items?.length,
1238
- originalItemsCount: originalDoc?.items?.length,
1239
- effectiveItemsCount: effectiveItems.length
1240
- });
1241
1361
  if (!effectiveItems.length) return {
1242
1362
  ...data,
1243
1363
  partnerCommission: 0,
@@ -1284,12 +1404,6 @@ const recalculateCartHook = (pluginConfig) => async ({ data, req, originalDoc })
1284
1404
  currencyCode: pluginConfig.defaultCurrency
1285
1405
  });
1286
1406
  calculatedSubtotal += itemPrice * (item.quantity ?? 1);
1287
- console.log("[RecalculateCart] Item processed", {
1288
- productId,
1289
- quantity: item.quantity,
1290
- priceUsed: itemPrice,
1291
- currentSubtotal: calculatedSubtotal
1292
- });
1293
1407
  return {
1294
1408
  ...item,
1295
1409
  product,
@@ -1318,16 +1432,30 @@ const recalculateCartHook = (pluginConfig) => async ({ data, req, originalDoc })
1318
1432
  id: programId
1319
1433
  }) : null;
1320
1434
  if (program) {
1435
+ const minOrderAmount = getProgramMinimumOrderAmount({
1436
+ program,
1437
+ allowedTotalCommissionTypes: pluginConfig.referralConfig.allowedTotalCommissionTypes
1438
+ });
1439
+ if (typeof minOrderAmount === "number" && calculatedSubtotal < minOrderAmount) {
1440
+ data.appliedReferralCode = null;
1441
+ data.partnerCommission = 0;
1442
+ data.customerDiscount = 0;
1443
+ data.total = calculatedSubtotal;
1444
+ return data;
1445
+ }
1321
1446
  const { partnerCommission, customerDiscount } = calculateCommissionAndDiscount({
1322
1447
  cartItems: enrichedItems,
1323
1448
  program,
1324
- currencyCode: pluginConfig.defaultCurrency
1449
+ currencyCode: pluginConfig.defaultCurrency,
1450
+ cartTotal: calculatedSubtotal,
1451
+ allowedTotalCommissionTypes: pluginConfig.referralConfig.allowedTotalCommissionTypes
1325
1452
  });
1326
1453
  const roundedCustomerDiscount = roundTo2(customerDiscount);
1327
1454
  data.partnerCommission = roundTo2(partnerCommission);
1328
1455
  data.customerDiscount = roundedCustomerDiscount;
1329
1456
  data.total = Math.max(0, calculatedSubtotal - roundedCustomerDiscount);
1330
1457
  } else {
1458
+ data.appliedReferralCode = null;
1331
1459
  data.partnerCommission = 0;
1332
1460
  data.customerDiscount = 0;
1333
1461
  data.total = calculatedSubtotal;
@@ -1348,12 +1476,6 @@ const recalculateCartHook = (pluginConfig) => async ({ data, req, originalDoc })
1348
1476
  coupon,
1349
1477
  cartTotal: calculatedSubtotal
1350
1478
  });
1351
- console.log("[RecalculateCart] Coupon Logic", {
1352
- appliedCoupon,
1353
- couponId: coupon.id,
1354
- cartTotal: calculatedSubtotal,
1355
- discountAmount
1356
- });
1357
1479
  data.discountAmount = discountAmount;
1358
1480
  const currentDiscount = data.customerDiscount || 0;
1359
1481
  data.total = Math.max(0, calculatedSubtotal - currentDiscount - discountAmount);
@@ -1365,6 +1487,13 @@ const recalculateCartHook = (pluginConfig) => async ({ data, req, originalDoc })
1365
1487
  //#endregion
1366
1488
  //#region src/utilities/sanitizePluginConfig.ts
1367
1489
  const sanitizePluginConfig = ({ pluginConfig }) => {
1490
+ const roleConfig = {
1491
+ roleFieldPaths: Array.isArray(pluginConfig?.roleConfig?.roleFieldPaths) && pluginConfig.roleConfig.roleFieldPaths.length > 0 ? pluginConfig.roleConfig.roleFieldPaths.filter((value) => typeof value === "string").map((value) => value.trim()).filter(Boolean) : ["role", "roles"],
1492
+ adminRoleValues: Array.isArray(pluginConfig?.roleConfig?.adminRoleValues) && pluginConfig.roleConfig.adminRoleValues.length > 0 ? pluginConfig.roleConfig.adminRoleValues.filter((value) => typeof value === "string").map((value) => value.trim()).filter(Boolean) : ["admin"],
1493
+ partnerRoleValues: Array.isArray(pluginConfig?.roleConfig?.partnerRoleValues) && pluginConfig.roleConfig.partnerRoleValues.length > 0 ? pluginConfig.roleConfig.partnerRoleValues.filter((value) => typeof value === "string").map((value) => value.trim()).filter(Boolean) : ["partner"],
1494
+ customRoleResolver: typeof pluginConfig?.roleConfig?.customRoleResolver === "function" ? pluginConfig.roleConfig.customRoleResolver : void 0
1495
+ };
1496
+ const normalizedAllowedTotalCommissionTypes = Array.isArray(pluginConfig?.referralConfig?.allowedTotalCommissionTypes) ? [...new Set(pluginConfig.referralConfig.allowedTotalCommissionTypes.filter((value) => value === "fixed" || value === "percentage"))] : [];
1368
1497
  return {
1369
1498
  enabled: !(pluginConfig?.enabled === false || typeof pluginConfig?.enabled === "string" && pluginConfig.enabled === "false"),
1370
1499
  enableReferrals: !!pluginConfig?.enableReferrals && (typeof pluginConfig?.enableReferrals !== "string" || pluginConfig.enableReferrals !== "false"),
@@ -1386,20 +1515,21 @@ const sanitizePluginConfig = ({ pluginConfig }) => {
1386
1515
  access: {
1387
1516
  canUseCoupons: typeof pluginConfig?.access?.canUseCoupons === "function" ? pluginConfig.access.canUseCoupons : () => true,
1388
1517
  canUseReferrals: typeof pluginConfig?.access?.canUseReferrals === "function" ? pluginConfig.access.canUseReferrals : () => false,
1389
- isAdmin: typeof pluginConfig?.access?.isAdmin === "function" ? pluginConfig.access.isAdmin : () => false,
1390
- isPartner: typeof pluginConfig?.access?.isPartner === "function" ? pluginConfig.access.isPartner : ({ req }) => {
1391
- const user = req?.user;
1392
- if (!user) return false;
1393
- if (user.role === "partner") return true;
1394
- if (Array.isArray(user.roles) && user.roles.includes("partner")) return true;
1395
- return false;
1396
- }
1518
+ isAdmin: typeof pluginConfig?.access?.isAdmin === "function" ? pluginConfig.access.isAdmin : ({ req }) => isAdminUser({
1519
+ user: req?.user,
1520
+ roleConfig
1521
+ }),
1522
+ isPartner: typeof pluginConfig?.access?.isPartner === "function" ? pluginConfig.access.isPartner : ({ req }) => isPartnerUser({
1523
+ user: req?.user,
1524
+ roleConfig
1525
+ })
1397
1526
  },
1398
1527
  referralConfig: {
1399
1528
  allowBothSystems: pluginConfig?.referralConfig?.allowBothSystems ?? false,
1400
1529
  singleCodePerCart: pluginConfig?.referralConfig?.singleCodePerCart ?? true,
1401
1530
  defaultPartnerSplit: pluginConfig?.referralConfig?.defaultPartnerSplit ?? 70,
1402
- defaultCustomerSplit: pluginConfig?.referralConfig?.defaultCustomerSplit ?? 30
1531
+ defaultCustomerSplit: pluginConfig?.referralConfig?.defaultCustomerSplit ?? 30,
1532
+ allowedTotalCommissionTypes: normalizedAllowedTotalCommissionTypes.length > 0 ? normalizedAllowedTotalCommissionTypes : ["fixed", "percentage"]
1403
1533
  },
1404
1534
  adminGroups: {
1405
1535
  couponsGroup: pluginConfig?.adminGroups?.couponsGroup ?? "Coupons",
@@ -1417,7 +1547,8 @@ const sanitizePluginConfig = ({ pluginConfig }) => {
1417
1547
  orderCustomerEmailField: typeof pluginConfig?.orderIntegration?.orderCustomerEmailField === "string" && pluginConfig.orderIntegration.orderCustomerEmailField.trim().length > 0 ? pluginConfig.orderIntegration.orderCustomerEmailField : "customerEmail",
1418
1548
  orderPaymentStatusField: typeof pluginConfig?.orderIntegration?.orderPaymentStatusField === "string" && pluginConfig.orderIntegration.orderPaymentStatusField.trim().length > 0 ? pluginConfig.orderIntegration.orderPaymentStatusField : "paymentStatus",
1419
1549
  orderPaidStatusValue: typeof pluginConfig?.orderIntegration?.orderPaidStatusValue === "string" ? pluginConfig.orderIntegration.orderPaidStatusValue : "paid"
1420
- }
1550
+ },
1551
+ roleConfig
1421
1552
  };
1422
1553
  };
1423
1554
 
@@ -1809,6 +1940,7 @@ exports.createCouponsCollection = createCouponsCollection;
1809
1940
  exports.createReferralCodesCollection = createReferralCodesCollection;
1810
1941
  exports.createReferralProgramsCollection = createReferralProgramsCollection;
1811
1942
  exports.getCartTotalWithDiscounts = getCartTotalWithDiscounts;
1943
+ exports.getProgramMinimumOrderAmount = getProgramMinimumOrderAmount;
1812
1944
  exports.payloadEcommerceCoupon = payloadEcommerceCouponPlugin;
1813
1945
  exports.recordCouponUsageForOrder = recordCouponUsageForOrder;
1814
1946
  exports.useCouponCode = useCouponCode;