@wtree/payload-ecommerce-coupon 3.77.2 → 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,gBAuNF,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,cAgLzB,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.role) && user.role.includes("admin") || Array.isArray(user.roles) && user.roles.includes("admin")) return true;
145
- if (user.role === "partner" || Array.isArray(user.role) && user.role.includes("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.role) && user.role.includes("admin") || Array.isArray(user.roles) && user.roles.includes("admin")) return true;
152
- if (user.role === "partner" || Array.isArray(user.role) && user.role.includes("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,11 +236,12 @@ const createReferralCodesCollection = (pluginConfig) => {
175
236
  type: "relationship",
176
237
  relationTo: "users",
177
238
  required: true,
178
- filterOptions: ({ data }) => {
179
- const user = data?.user;
180
- if (!user) return false;
181
- if (user.role === "partner" || Array.isArray(user.role) && user.role.includes("partner") || Array.isArray(user.roles) && user.roles.includes("partner")) return true;
182
- return false;
239
+ filterOptions: ({ req, user }) => {
240
+ if (isAdminUser({
241
+ user: user || req?.user,
242
+ roleConfig
243
+ }) || access.isAdmin?.({ req })) return true;
244
+ return buildPartnerUserFilterWhere({ roleConfig });
183
245
  },
184
246
  admin: { description: "The partner who owns this referral code" }
185
247
  },
@@ -257,7 +319,10 @@ const createReferralCodesCollection = (pluginConfig) => {
257
319
  if (operation === "create" && !data.code && data.partner) data.code = `REF-${Date.now().toString(36)}-${Math.random().toString(36).substring(2, 8)}`.toUpperCase();
258
320
  if (operation === "create" && req.user) {
259
321
  const user = req.user;
260
- if (user.role === "partner" || Array.isArray(user.role) && user.role.includes("partner")) data.partner = user.id;
322
+ if (isPartnerUser({
323
+ user,
324
+ roleConfig
325
+ })) data.partner = user.id;
261
326
  }
262
327
  return data;
263
328
  }] },
@@ -279,6 +344,7 @@ const deriveCustomerSplit = (partnerSplit) => {
279
344
  };
280
345
  const createReferralProgramsCollection = (pluginConfig) => {
281
346
  const { collections, access, defaultCurrency, adminGroups, referralConfig } = pluginConfig;
347
+ const allowedTotalCommissionTypes = referralConfig.allowedTotalCommissionTypes;
282
348
  return {
283
349
  slug: collections.referralProgramsSlug,
284
350
  admin: {
@@ -301,7 +367,7 @@ const createReferralProgramsCollection = (pluginConfig) => {
301
367
  data.commissionRules = data.commissionRules.map((rule, index) => {
302
368
  const r = rule;
303
369
  if (!r.totalCommission) throw new Error(`Commission rule ${index + 1}: Total Commission is required`);
304
- 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(", ")}`);
305
371
  const totalValue = toNumber(r.totalCommission.value);
306
372
  if (totalValue == null || totalValue < 0) throw new Error(`Commission rule ${index + 1}: Total Commission value must be a non-negative number`);
307
373
  if (r.totalCommission.type === "percentage" && totalValue > 100) throw new Error(`Commission rule ${index + 1}: Percentage Total Commission cannot exceed 100`);
@@ -313,6 +379,8 @@ const createReferralProgramsCollection = (pluginConfig) => {
313
379
  const partnerSplit = toNumber(r.partnerSplit);
314
380
  if (partnerSplit == null || partnerSplit < 0 || partnerSplit > 100) throw new Error(`Commission rule ${index + 1}: Partner Split must be between 0 and 100`);
315
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`);
316
384
  return {
317
385
  ...rule,
318
386
  appliesTo: appliesTo === "categories" ? "segments" : appliesTo,
@@ -322,7 +390,8 @@ const createReferralProgramsCollection = (pluginConfig) => {
322
390
  maxAmount: maxAmount ?? null
323
391
  },
324
392
  partnerSplit,
325
- customerSplit
393
+ customerSplit,
394
+ minOrderAmount: minOrderAmount ?? null
326
395
  };
327
396
  });
328
397
  return data;
@@ -412,14 +481,11 @@ const createReferralProgramsCollection = (pluginConfig) => {
412
481
  name: "type",
413
482
  type: "select",
414
483
  required: true,
415
- options: [{
416
- label: "Fixed Amount",
417
- value: "fixed"
418
- }, {
419
- label: "Percentage of Order",
420
- value: "percentage"
421
- }],
422
- 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"
423
489
  },
424
490
  {
425
491
  name: "value",
@@ -445,6 +511,12 @@ const createReferralProgramsCollection = (pluginConfig) => {
445
511
  defaultValue: referralConfig.defaultPartnerSplit,
446
512
  admin: { description: "Percentage of total commission given to Partner (0-100)" }
447
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
+ },
448
520
  {
449
521
  name: "customerSplit",
450
522
  type: "number",
@@ -531,6 +603,7 @@ function relationId(value) {
531
603
  if (typeof value === "object" && (typeof value.id === "string" || typeof value.id === "number")) return value.id;
532
604
  return null;
533
605
  }
606
+ const allowedCommissionTypesSet = (allowed) => new Set((allowed && allowed.length ? allowed : ["fixed", "percentage"]).map((v) => v));
534
607
  function normalizeIds(values) {
535
608
  if (!Array.isArray(values)) return [];
536
609
  return values.map(relationId).filter((v) => v != null);
@@ -543,8 +616,10 @@ function getRuleSplits(rule) {
543
616
  customerSplit: typeof rule.customerSplit === "number" ? rule.customerSplit : typeof rule.refereeSplit === "number" ? rule.refereeSplit : 100 - partnerRaw
544
617
  };
545
618
  }
546
- function calculateItemRewardByRule({ rule, itemTotal, quantity }) {
619
+ function calculateItemRewardByRule({ rule, itemTotal, quantity, allowedTotalCommissionTypes }) {
620
+ const allowedTypes = allowedCommissionTypesSet(allowedTotalCommissionTypes);
547
621
  if (rule.totalCommission) {
622
+ if (!allowedTypes.has(rule.totalCommission.type)) return null;
548
623
  const splits = getRuleSplits(rule);
549
624
  if (!splits) return null;
550
625
  let totalPot = 0;
@@ -583,21 +658,27 @@ function getItemCategoryIds(item) {
583
658
  function getItemTagIds(item) {
584
659
  return Array.isArray(item?.product?.tags) ? normalizeIds(item.product.tags) : [];
585
660
  }
586
- 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
+ });
587
668
  const productId = relationId(item.product);
588
669
  const itemCategoryIds = new Set(getItemCategoryIds(item));
589
670
  const itemTagIds = new Set(getItemTagIds(item));
590
671
  const candidates = [
591
- rules.filter((r) => r.appliesTo === "products" && normalizeIds(r.products).some((id) => productId != null && id === productId)),
592
- rules.filter((r) => {
672
+ eligibleRules.filter((r) => r.appliesTo === "products" && normalizeIds(r.products).some((id) => productId != null && id === productId)),
673
+ eligibleRules.filter((r) => {
593
674
  if (!(r.appliesTo === "segments" || r.appliesTo === "categories")) return false;
594
675
  return normalizeIds(r.categories).some((id) => itemCategoryIds.has(id));
595
676
  }),
596
- rules.filter((r) => {
677
+ eligibleRules.filter((r) => {
597
678
  if (r.appliesTo !== "segments") return false;
598
679
  return normalizeIds(r.tags).some((id) => itemTagIds.has(id));
599
680
  }),
600
- rules.filter((r) => r.appliesTo === "all")
681
+ eligibleRules.filter((r) => r.appliesTo === "all")
601
682
  ].find((level) => level.length > 0) ?? [];
602
683
  if (!candidates.length) return null;
603
684
  let best = null;
@@ -605,7 +686,8 @@ function selectBestRuleForItem({ rules, item, itemTotal, quantity }) {
605
686
  const reward = calculateItemRewardByRule({
606
687
  rule,
607
688
  itemTotal,
608
- quantity
689
+ quantity,
690
+ allowedTotalCommissionTypes
609
691
  });
610
692
  if (!reward) continue;
611
693
  if (!best) {
@@ -629,7 +711,18 @@ function selectBestRuleForItem({ rules, item, itemTotal, quantity }) {
629
711
  }
630
712
  return best;
631
713
  }
632
- 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 }) {
633
726
  const rules = Array.isArray(program?.commissionRules) ? program.commissionRules : [];
634
727
  if (!rules.length) return {
635
728
  partnerCommission: 0,
@@ -654,7 +747,9 @@ function calculateCommissionAndDiscount({ cartItems, program, currencyCode = "AE
654
747
  product
655
748
  },
656
749
  itemTotal,
657
- quantity
750
+ quantity,
751
+ cartTotal,
752
+ allowedTotalCommissionTypes
658
753
  });
659
754
  if (!bestMatch) continue;
660
755
  totalPartnerCommission += bestMatch.reward.partner;
@@ -882,10 +977,20 @@ async function handleReferralCode({ payload, code, cartID, cart, customerEmail:
882
977
  error: "Referral code already applied to this cart"
883
978
  }, { status: 400 });
884
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 });
885
988
  const { partnerCommission, customerDiscount } = calculateCommissionAndDiscount({
886
989
  cartItems: cart.items || [],
887
990
  program,
888
- currencyCode: pluginConfig.defaultCurrency
991
+ currencyCode: pluginConfig.defaultCurrency,
992
+ cartTotal,
993
+ allowedTotalCommissionTypes: pluginConfig.referralConfig.allowedTotalCommissionTypes
889
994
  });
890
995
  const roundedPartnerCommission = roundTo2(partnerCommission);
891
996
  const roundedCustomerDiscount = roundTo2(customerDiscount);
@@ -925,8 +1030,14 @@ const partnerStatsHandler = ({ pluginConfig }) => async (req) => {
925
1030
  error: "Authentication required"
926
1031
  }, { status: 401 });
927
1032
  const typedUser = user;
928
- const isPartner = typedUser.role === "partner" || Array.isArray(typedUser.role) && typedUser.role?.includes("partner") || Array.isArray(typedUser.roles) && typedUser.roles?.includes("partner");
929
- const isAdmin = typedUser.role === "admin" || Array.isArray(typedUser.role) && typedUser.role?.includes("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 });
930
1041
  if (!isPartner && !isAdmin) return Response.json({
931
1042
  success: false,
932
1043
  error: "Partner access required"
@@ -1206,12 +1317,22 @@ async function validateReferralCode({ payload, code, cartID, pluginConfig }) {
1206
1317
  id: cartID,
1207
1318
  depth: 2
1208
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 });
1209
1329
  const { partnerCommission, customerDiscount } = calculateCommissionAndDiscount({
1210
1330
  cartItems: cart?.items || [],
1211
1331
  program,
1212
- currencyCode: pluginConfig.defaultCurrency
1332
+ currencyCode: pluginConfig.defaultCurrency,
1333
+ cartTotal,
1334
+ allowedTotalCommissionTypes: pluginConfig.referralConfig.allowedTotalCommissionTypes
1213
1335
  });
1214
- const cartTotal = cart ? cart.subtotal || cart.total || 0 : 0;
1215
1336
  const cappedCustomerDiscount = cartTotal > 0 ? Math.min(customerDiscount, cartTotal) : customerDiscount;
1216
1337
  const roundedPartnerCommission = roundTo2(partnerCommission);
1217
1338
  const roundedCustomerDiscount = roundTo2(cappedCustomerDiscount);
@@ -1237,12 +1358,6 @@ const validateCouponEndpoint = ({ pluginConfig }) => ({
1237
1358
  const recalculateCartHook = (pluginConfig) => async ({ data, req, originalDoc }) => {
1238
1359
  if (!req.payload) return data;
1239
1360
  const effectiveItems = data.items || originalDoc?.items || [];
1240
- console.log("[RecalculateCart] Hook triggered", {
1241
- hasDataItems: !!data.items,
1242
- dataItemsCount: data.items?.length,
1243
- originalItemsCount: originalDoc?.items?.length,
1244
- effectiveItemsCount: effectiveItems.length
1245
- });
1246
1361
  if (!effectiveItems.length) return {
1247
1362
  ...data,
1248
1363
  partnerCommission: 0,
@@ -1289,12 +1404,6 @@ const recalculateCartHook = (pluginConfig) => async ({ data, req, originalDoc })
1289
1404
  currencyCode: pluginConfig.defaultCurrency
1290
1405
  });
1291
1406
  calculatedSubtotal += itemPrice * (item.quantity ?? 1);
1292
- console.log("[RecalculateCart] Item processed", {
1293
- productId,
1294
- quantity: item.quantity,
1295
- priceUsed: itemPrice,
1296
- currentSubtotal: calculatedSubtotal
1297
- });
1298
1407
  return {
1299
1408
  ...item,
1300
1409
  product,
@@ -1323,16 +1432,30 @@ const recalculateCartHook = (pluginConfig) => async ({ data, req, originalDoc })
1323
1432
  id: programId
1324
1433
  }) : null;
1325
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
+ }
1326
1446
  const { partnerCommission, customerDiscount } = calculateCommissionAndDiscount({
1327
1447
  cartItems: enrichedItems,
1328
1448
  program,
1329
- currencyCode: pluginConfig.defaultCurrency
1449
+ currencyCode: pluginConfig.defaultCurrency,
1450
+ cartTotal: calculatedSubtotal,
1451
+ allowedTotalCommissionTypes: pluginConfig.referralConfig.allowedTotalCommissionTypes
1330
1452
  });
1331
1453
  const roundedCustomerDiscount = roundTo2(customerDiscount);
1332
1454
  data.partnerCommission = roundTo2(partnerCommission);
1333
1455
  data.customerDiscount = roundedCustomerDiscount;
1334
1456
  data.total = Math.max(0, calculatedSubtotal - roundedCustomerDiscount);
1335
1457
  } else {
1458
+ data.appliedReferralCode = null;
1336
1459
  data.partnerCommission = 0;
1337
1460
  data.customerDiscount = 0;
1338
1461
  data.total = calculatedSubtotal;
@@ -1353,12 +1476,6 @@ const recalculateCartHook = (pluginConfig) => async ({ data, req, originalDoc })
1353
1476
  coupon,
1354
1477
  cartTotal: calculatedSubtotal
1355
1478
  });
1356
- console.log("[RecalculateCart] Coupon Logic", {
1357
- appliedCoupon,
1358
- couponId: coupon.id,
1359
- cartTotal: calculatedSubtotal,
1360
- discountAmount
1361
- });
1362
1479
  data.discountAmount = discountAmount;
1363
1480
  const currentDiscount = data.customerDiscount || 0;
1364
1481
  data.total = Math.max(0, calculatedSubtotal - currentDiscount - discountAmount);
@@ -1370,6 +1487,13 @@ const recalculateCartHook = (pluginConfig) => async ({ data, req, originalDoc })
1370
1487
  //#endregion
1371
1488
  //#region src/utilities/sanitizePluginConfig.ts
1372
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"))] : [];
1373
1497
  return {
1374
1498
  enabled: !(pluginConfig?.enabled === false || typeof pluginConfig?.enabled === "string" && pluginConfig.enabled === "false"),
1375
1499
  enableReferrals: !!pluginConfig?.enableReferrals && (typeof pluginConfig?.enableReferrals !== "string" || pluginConfig.enableReferrals !== "false"),
@@ -1391,20 +1515,21 @@ const sanitizePluginConfig = ({ pluginConfig }) => {
1391
1515
  access: {
1392
1516
  canUseCoupons: typeof pluginConfig?.access?.canUseCoupons === "function" ? pluginConfig.access.canUseCoupons : () => true,
1393
1517
  canUseReferrals: typeof pluginConfig?.access?.canUseReferrals === "function" ? pluginConfig.access.canUseReferrals : () => false,
1394
- isAdmin: typeof pluginConfig?.access?.isAdmin === "function" ? pluginConfig.access.isAdmin : () => false,
1395
- isPartner: typeof pluginConfig?.access?.isPartner === "function" ? pluginConfig.access.isPartner : ({ req }) => {
1396
- const user = req?.user;
1397
- if (!user) return false;
1398
- if (user.role === "partner") return true;
1399
- if (Array.isArray(user.roles) && user.roles.includes("partner")) return true;
1400
- return false;
1401
- }
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
+ })
1402
1526
  },
1403
1527
  referralConfig: {
1404
1528
  allowBothSystems: pluginConfig?.referralConfig?.allowBothSystems ?? false,
1405
1529
  singleCodePerCart: pluginConfig?.referralConfig?.singleCodePerCart ?? true,
1406
1530
  defaultPartnerSplit: pluginConfig?.referralConfig?.defaultPartnerSplit ?? 70,
1407
- defaultCustomerSplit: pluginConfig?.referralConfig?.defaultCustomerSplit ?? 30
1531
+ defaultCustomerSplit: pluginConfig?.referralConfig?.defaultCustomerSplit ?? 30,
1532
+ allowedTotalCommissionTypes: normalizedAllowedTotalCommissionTypes.length > 0 ? normalizedAllowedTotalCommissionTypes : ["fixed", "percentage"]
1408
1533
  },
1409
1534
  adminGroups: {
1410
1535
  couponsGroup: pluginConfig?.adminGroups?.couponsGroup ?? "Coupons",
@@ -1422,7 +1547,8 @@ const sanitizePluginConfig = ({ pluginConfig }) => {
1422
1547
  orderCustomerEmailField: typeof pluginConfig?.orderIntegration?.orderCustomerEmailField === "string" && pluginConfig.orderIntegration.orderCustomerEmailField.trim().length > 0 ? pluginConfig.orderIntegration.orderCustomerEmailField : "customerEmail",
1423
1548
  orderPaymentStatusField: typeof pluginConfig?.orderIntegration?.orderPaymentStatusField === "string" && pluginConfig.orderIntegration.orderPaymentStatusField.trim().length > 0 ? pluginConfig.orderIntegration.orderPaymentStatusField : "paymentStatus",
1424
1549
  orderPaidStatusValue: typeof pluginConfig?.orderIntegration?.orderPaidStatusValue === "string" ? pluginConfig.orderIntegration.orderPaidStatusValue : "paid"
1425
- }
1550
+ },
1551
+ roleConfig
1426
1552
  };
1427
1553
  };
1428
1554
 
@@ -1814,6 +1940,7 @@ exports.createCouponsCollection = createCouponsCollection;
1814
1940
  exports.createReferralCodesCollection = createReferralCodesCollection;
1815
1941
  exports.createReferralProgramsCollection = createReferralProgramsCollection;
1816
1942
  exports.getCartTotalWithDiscounts = getCartTotalWithDiscounts;
1943
+ exports.getProgramMinimumOrderAmount = getProgramMinimumOrderAmount;
1817
1944
  exports.payloadEcommerceCoupon = payloadEcommerceCouponPlugin;
1818
1945
  exports.recordCouponUsageForOrder = recordCouponUsageForOrder;
1819
1946
  exports.useCouponCode = useCouponCode;