@wtree/payload-ecommerce-coupon 3.77.4 → 3.77.6

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.
Files changed (53) hide show
  1. package/README.md +274 -684
  2. package/dist/client/hooks.d.ts +21 -13
  3. package/dist/client/hooks.d.ts.map +1 -1
  4. package/dist/client/index.d.ts +6 -6
  5. package/dist/client/index.d.ts.map +1 -1
  6. package/dist/collections/createCouponsCollection.d.ts +2 -2
  7. package/dist/collections/createCouponsCollection.d.ts.map +1 -1
  8. package/dist/collections/createReferralCodesCollection.d.ts +2 -2
  9. package/dist/collections/createReferralCodesCollection.d.ts.map +1 -1
  10. package/dist/collections/createReferralProgramsCollection.d.ts +2 -2
  11. package/dist/collections/createReferralProgramsCollection.d.ts.map +1 -1
  12. package/dist/components/PartnerDashboard/EarningsSummary.d.ts +2 -2
  13. package/dist/components/PartnerDashboard/EarningsSummary.d.ts.map +1 -1
  14. package/dist/components/PartnerDashboard/RecentReferrals.d.ts +3 -3
  15. package/dist/components/PartnerDashboard/RecentReferrals.d.ts.map +1 -1
  16. package/dist/components/PartnerDashboard/ReferralCodes.d.ts +3 -3
  17. package/dist/components/PartnerDashboard/ReferralCodes.d.ts.map +1 -1
  18. package/dist/components/PartnerDashboard/ReferralPerformance.d.ts +2 -2
  19. package/dist/components/PartnerDashboard/ReferralPerformance.d.ts.map +1 -1
  20. package/dist/components/PartnerDashboard/index.d.ts +2 -2
  21. package/dist/components/PartnerDashboard/index.d.ts.map +1 -1
  22. package/dist/endpoints/applyCoupon.d.ts +2 -2
  23. package/dist/endpoints/applyCoupon.d.ts.map +1 -1
  24. package/dist/endpoints/partnerStats.d.ts +2 -2
  25. package/dist/endpoints/partnerStats.d.ts.map +1 -1
  26. package/dist/endpoints/validateCoupon.d.ts +2 -2
  27. package/dist/endpoints/validateCoupon.d.ts.map +1 -1
  28. package/dist/hooks/recalculateCart.d.ts +2 -2
  29. package/dist/hooks/recalculateCart.d.ts.map +1 -1
  30. package/dist/index.d.ts +10 -10
  31. package/dist/index.d.ts.map +1 -1
  32. package/dist/index.js +1123 -543
  33. package/dist/index.js.map +1 -1
  34. package/dist/index.mjs +1122 -541
  35. package/dist/index.mjs.map +1 -1
  36. package/dist/plugin.d.ts +2 -2
  37. package/dist/plugin.d.ts.map +1 -1
  38. package/dist/types.d.ts +103 -9
  39. package/dist/types.d.ts.map +1 -1
  40. package/dist/utilities/calculateValues.d.ts +2 -2
  41. package/dist/utilities/calculateValues.d.ts.map +1 -1
  42. package/dist/utilities/getCartTotalWithDiscounts.d.ts.map +1 -1
  43. package/dist/utilities/migrateReferralRulesV2.d.ts.map +1 -1
  44. package/dist/utilities/pricing.d.ts.map +1 -1
  45. package/dist/utilities/pushTypeScriptProperties.d.ts +1 -1
  46. package/dist/utilities/pushTypeScriptProperties.d.ts.map +1 -1
  47. package/dist/utilities/recordCouponUsageForOrder.d.ts +11 -17
  48. package/dist/utilities/recordCouponUsageForOrder.d.ts.map +1 -1
  49. package/dist/utilities/sanitizePluginConfig.d.ts +1 -1
  50. package/dist/utilities/sanitizePluginConfig.d.ts.map +1 -1
  51. package/dist/utilities/userRoles.d.ts +3 -3
  52. package/dist/utilities/userRoles.d.ts.map +1 -1
  53. package/package.json +8 -8
package/dist/index.js CHANGED
@@ -1,8 +1,8 @@
1
- Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
2
-
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
3
2
  //#region src/collections/createCouponsCollection.ts
4
3
  const createCouponsCollection = (pluginConfig) => {
5
- const { collections, access, defaultCurrency, adminGroups } = pluginConfig;
4
+ const { collections, access, defaultCurrency, adminGroups, integration } = pluginConfig;
5
+ const usersSlug = integration.collections.usersSlug;
6
6
  return {
7
7
  slug: collections.couponsSlug,
8
8
  admin: {
@@ -30,6 +30,16 @@ const createCouponsCollection = (pluginConfig) => {
30
30
  unique: true,
31
31
  admin: { description: "The coupon code that customers will enter" }
32
32
  },
33
+ {
34
+ name: "normalizedCode",
35
+ type: "text",
36
+ unique: true,
37
+ index: true,
38
+ admin: {
39
+ hidden: true,
40
+ description: "Uppercased, trimmed code used for fast case-insensitive lookups"
41
+ }
42
+ },
33
43
  {
34
44
  name: "description",
35
45
  type: "text",
@@ -105,21 +115,33 @@ const createCouponsCollection = (pluginConfig) => {
105
115
  {
106
116
  name: "createdBy",
107
117
  type: "relationship",
108
- relationTo: "users",
118
+ relationTo: usersSlug,
109
119
  admin: {
110
120
  readOnly: true,
111
121
  position: "sidebar"
112
122
  }
113
123
  }
114
124
  ],
115
- hooks: { beforeChange: [({ operation, req, data }) => {
116
- if (operation === "create" && req.user) data.createdBy = req.user.id;
117
- return data;
118
- }] },
125
+ hooks: {
126
+ beforeValidate: [({ data }) => {
127
+ if (data && typeof data.code === "string") {
128
+ data.code = data.code.trim();
129
+ data.normalizedCode = data.code.toUpperCase();
130
+ }
131
+ return data;
132
+ }],
133
+ beforeChange: [({ operation, req, data }) => {
134
+ if (data && typeof data.code === "string") {
135
+ data.code = data.code.trim();
136
+ data.normalizedCode = data.code.toUpperCase();
137
+ }
138
+ if (operation === "create" && req.user && !data.createdBy) data.createdBy = req.user.id;
139
+ return data;
140
+ }]
141
+ },
119
142
  timestamps: true
120
143
  };
121
144
  };
122
-
123
145
  //#endregion
124
146
  //#region src/utilities/userRoles.ts
125
147
  function readByPath(input, path) {
@@ -168,17 +190,19 @@ const buildPartnerUserFilterWhere = ({ roleConfig }) => {
168
190
  if (conditions.length === 1) return conditions[0];
169
191
  return { or: conditions };
170
192
  };
171
-
172
193
  //#endregion
173
194
  //#region src/collections/createReferralCodesCollection.ts
195
+ const normalizeCode$2 = (value) => typeof value === "string" ? value.trim().toUpperCase() : "";
174
196
  const createReferralCodesCollection = (pluginConfig) => {
175
- const { collections, access, adminGroups, defaultCurrency, roleConfig } = pluginConfig;
197
+ const { collections, access, adminGroups, defaultCurrency, roleConfig, policies, integration } = pluginConfig;
198
+ const usersSlug = integration.collections.usersSlug;
176
199
  return {
177
200
  slug: collections.referralCodesSlug,
178
201
  admin: {
179
202
  useAsTitle: "code",
180
203
  defaultColumns: [
181
204
  "code",
205
+ "normalizedCode",
182
206
  "partner",
183
207
  "program",
184
208
  "usageCount",
@@ -187,34 +211,59 @@ const createReferralCodesCollection = (pluginConfig) => {
187
211
  group: adminGroups.referralsGroup
188
212
  },
189
213
  access: {
190
- read: ({ req }) => {
214
+ read: async ({ req }) => {
191
215
  const user = req?.user;
192
216
  if (!user) return false;
193
217
  if (isAdminUser({
194
218
  user,
195
219
  roleConfig
196
- }) || access.isAdmin?.({ req })) return true;
220
+ }) || await Promise.resolve(access.isAdmin?.({ req }))) return true;
221
+ if (!await Promise.resolve(policies.canApplyReferral({
222
+ req,
223
+ user,
224
+ payload: req?.payload
225
+ }))) return false;
197
226
  if (isPartnerUser({
198
227
  user,
199
228
  roleConfig
200
- }) || access.isPartner?.({ req })) return { partner: { equals: user.id } };
229
+ }) || await Promise.resolve(access.isPartner?.({ req }))) return { partner: { equals: user.id } };
201
230
  return access.canUseReferrals ? access.canUseReferrals({ req }) : false;
202
231
  },
203
- create: ({ req }) => {
232
+ create: async ({ req }) => {
204
233
  const user = req?.user;
205
234
  if (!user) return false;
235
+ if (!await Promise.resolve(policies.canApplyReferral({
236
+ req,
237
+ user,
238
+ payload: req?.payload
239
+ }))) return false;
206
240
  if (isAdminUser({
207
241
  user,
208
242
  roleConfig
209
- }) || access.isAdmin?.({ req })) return true;
243
+ }) || await Promise.resolve(access.isAdmin?.({ req }))) return true;
210
244
  if (isPartnerUser({
211
245
  user,
212
246
  roleConfig
213
- }) || access.isPartner?.({ req })) return true;
247
+ }) || await Promise.resolve(access.isPartner?.({ req }))) return true;
214
248
  return access.isAdmin ? access.isAdmin({ req }) : false;
215
249
  },
216
- update: access.isAdmin || (() => false),
217
- delete: access.isAdmin || (() => false)
250
+ update: async ({ req }) => {
251
+ const user = req?.user;
252
+ if (!user) return false;
253
+ if (isAdminUser({
254
+ user,
255
+ roleConfig
256
+ }) || await Promise.resolve(access.isAdmin?.({ req }))) return true;
257
+ return false;
258
+ },
259
+ delete: async ({ req }) => {
260
+ const user = req?.user;
261
+ if (!user) return false;
262
+ return isAdminUser({
263
+ user,
264
+ roleConfig
265
+ }) || await Promise.resolve(access.isAdmin?.({ req }));
266
+ }
218
267
  },
219
268
  fields: [
220
269
  {
@@ -224,6 +273,17 @@ const createReferralCodesCollection = (pluginConfig) => {
224
273
  unique: true,
225
274
  admin: { description: "The referral code that customers will enter" }
226
275
  },
276
+ {
277
+ name: "normalizedCode",
278
+ type: "text",
279
+ required: true,
280
+ unique: true,
281
+ index: true,
282
+ admin: {
283
+ readOnly: true,
284
+ description: "Uppercased normalized code for fast case-insensitive lookup"
285
+ }
286
+ },
227
287
  {
228
288
  name: "program",
229
289
  type: "relationship",
@@ -234,16 +294,16 @@ const createReferralCodesCollection = (pluginConfig) => {
234
294
  {
235
295
  name: "partner",
236
296
  type: "relationship",
237
- relationTo: "users",
297
+ relationTo: usersSlug,
238
298
  required: true,
239
- filterOptions: ({ req, user }) => {
299
+ filterOptions: async ({ req, user }) => {
240
300
  if (isAdminUser({
241
301
  user: user || req?.user,
242
302
  roleConfig
243
- }) || access.isAdmin?.({ req })) return true;
303
+ }) || await Promise.resolve(access.isAdmin?.({ req }))) return true;
244
304
  return buildPartnerUserFilterWhere({ roleConfig });
245
305
  },
246
- admin: { description: "The partner who owns this referral code" }
306
+ admin: { description: `The partner who owns this referral code (relation: ${usersSlug})` }
247
307
  },
248
308
  {
249
309
  name: "isActive",
@@ -315,40 +375,41 @@ const createReferralCodesCollection = (pluginConfig) => {
315
375
  }
316
376
  }
317
377
  ],
318
- hooks: { beforeChange: [({ operation, req, data }) => {
319
- if (operation === "create" && !data.code && data.partner) data.code = `REF-${Date.now().toString(36)}-${Math.random().toString(36).substring(2, 8)}`.toUpperCase();
320
- if (operation === "create" && req.user) {
321
- const user = req.user;
322
- if (isPartnerUser({
323
- user,
324
- roleConfig
325
- })) data.partner = user.id;
326
- }
327
- return data;
328
- }] },
378
+ hooks: {
379
+ beforeValidate: [({ data }) => {
380
+ if (!data) return data;
381
+ data.normalizedCode = normalizeCode$2(data.code);
382
+ return data;
383
+ }],
384
+ beforeChange: [({ operation, req, data }) => {
385
+ if (!data) return data;
386
+ if (operation === "create" && !data.code && data.partner) data.code = `REF-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`.toUpperCase();
387
+ data.normalizedCode = normalizeCode$2(data.code);
388
+ if (operation === "create" && req.user) {
389
+ const user = req.user;
390
+ if (isPartnerUser({
391
+ user,
392
+ roleConfig
393
+ }) && user.id != null) data.partner = user.id;
394
+ }
395
+ return data;
396
+ }]
397
+ },
329
398
  timestamps: true
330
399
  };
331
400
  };
332
-
333
401
  //#endregion
334
402
  //#region src/collections/createReferralProgramsCollection.ts
335
403
  function toNumber(value) {
336
404
  return typeof value === "number" && Number.isFinite(value) ? value : null;
337
405
  }
338
- const deriveCustomerSplit = (partnerSplit, totalCommission) => {
339
- if (totalCommission?.type === "fixed" && totalCommission.value == null) {
340
- const partner = toNumber(partnerSplit);
341
- return partner != null ? partner : 0;
342
- }
343
- const partner = toNumber(partnerSplit);
344
- if (partner == null) return 0;
345
- if (partner < 0) return 100;
346
- if (partner > 100) return 0;
347
- return 100 - partner;
348
- };
406
+ function toCents(value) {
407
+ return Math.round(value * 100);
408
+ }
349
409
  const createReferralProgramsCollection = (pluginConfig) => {
350
- const { collections, access, defaultCurrency, adminGroups, referralConfig } = pluginConfig;
410
+ const { collections, access, defaultCurrency, adminGroups, referralConfig, integration } = pluginConfig;
351
411
  const allowedTotalCommissionTypes = referralConfig.allowedTotalCommissionTypes;
412
+ const relationSlugs = integration.collections;
352
413
  return {
353
414
  slug: collections.referralProgramsSlug,
354
415
  admin: {
@@ -372,34 +433,71 @@ const createReferralProgramsCollection = (pluginConfig) => {
372
433
  const r = rule;
373
434
  if (!r.totalCommission) throw new Error(`Commission rule ${index + 1}: Total Commission is required`);
374
435
  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(", ")}`);
436
+ const type = r.totalCommission.type;
375
437
  const totalValue = toNumber(r.totalCommission.value);
376
- if (r.totalCommission.type === "percentage" && (totalValue == null || totalValue < 0)) throw new Error(`Commission rule ${index + 1}: Total Commission value must be a non-negative number`);
377
- if (r.totalCommission.type === "percentage" && totalValue && totalValue > 100) throw new Error(`Commission rule ${index + 1}: Percentage Total Commission cannot exceed 100`);
378
438
  const maxAmount = toNumber(r.totalCommission.maxAmount);
439
+ if (type === "percentage") {
440
+ if (totalValue == null || totalValue < 0) throw new Error(`Commission rule ${index + 1}: Total Commission value must be a non-negative number`);
441
+ if (totalValue > 100) throw new Error(`Commission rule ${index + 1}: Percentage Total Commission cannot exceed 100`);
442
+ }
379
443
  if (maxAmount != null && maxAmount < 0) throw new Error(`Commission rule ${index + 1}: Max Amount must be a non-negative number`);
380
444
  const appliesTo = r.appliesTo ?? "all";
381
445
  if (appliesTo === "products" && (!r.products || r.products.length === 0)) throw new Error(`Commission rule ${index + 1}: At least one product is required`);
382
446
  if ((appliesTo === "segments" || appliesTo === "categories") && (!r.categories || r.categories.length === 0) && (!r.tags || r.tags.length === 0)) throw new Error(`Commission rule ${index + 1}: At least one category or tag is required`);
383
- const partnerSplit = toNumber(r.partnerSplit);
384
- if (partnerSplit == null || partnerSplit < 0) throw new Error(`Commission rule ${index + 1}: Partner Split must be a non-negative number`);
385
- const hasFixedValue = r.totalCommission.type === "fixed" && toNumber(r.totalCommission.value) != null;
386
- if (!hasFixedValue && r.totalCommission.type !== "fixed" && partnerSplit > 100) throw new Error(`Commission rule ${index + 1}: Partner Split must be between 0 and 100`);
387
- if (hasFixedValue && partnerSplit > 100) throw new Error(`Commission rule ${index + 1}: Partner Split must be between 0 and 100`);
388
- let customerSplit = null;
389
- if (r.totalCommission.type === "fixed" && !hasFixedValue) {
390
- customerSplit = toNumber(r.customerSplit);
391
- if (customerSplit == null || customerSplit < 0) throw new Error(`Commission rule ${index + 1}: For fixed commissions with no value, both partnerSplit and customerSplit must be non-negative numbers`);
392
- } else customerSplit = 100 - partnerSplit;
447
+ let partnerSplit;
448
+ let customerSplit;
449
+ let partnerPercent = null;
450
+ let customerPercent = null;
451
+ let partnerAmount = null;
452
+ let customerAmount = null;
453
+ if (type === "percentage") {
454
+ const partnerPctInput = toNumber(r.partnerPercent) ?? toNumber(r.partnerSplit);
455
+ if (partnerPctInput == null || partnerPctInput < 0 || partnerPctInput > 100) throw new Error(`Commission rule ${index + 1}: Partner Split must be between 0 and 100`);
456
+ const customerPctComputed = 100 - partnerPctInput;
457
+ if (customerPctComputed < 0 || customerPctComputed > 100) throw new Error(`Commission rule ${index + 1}: Customer percentage must be between 0 and 100`);
458
+ partnerPercent = partnerPctInput;
459
+ customerPercent = customerPctComputed;
460
+ partnerSplit = partnerPctInput;
461
+ customerSplit = customerPctComputed;
462
+ } else {
463
+ const partnerAmountInput = toNumber(r.partnerAmount);
464
+ const customerAmountInput = toNumber(r.customerAmount);
465
+ const legacyPartnerSplitInput = toNumber(r.partnerSplit);
466
+ const legacyCustomerSplitInput = toNumber(r.customerSplit);
467
+ const hasNewFixedInputs = partnerAmountInput != null || customerAmountInput != null;
468
+ const hasLegacyFixedInputs = legacyPartnerSplitInput != null || legacyCustomerSplitInput != null;
469
+ if (hasNewFixedInputs) {
470
+ if (partnerAmountInput == null || partnerAmountInput < 0) throw new Error(`Commission rule ${index + 1}: Partner fixed amount must be a non-negative number`);
471
+ if (customerAmountInput == null || customerAmountInput < 0) throw new Error(`Commission rule ${index + 1}: Customer fixed amount must be a non-negative number`);
472
+ partnerAmount = partnerAmountInput;
473
+ customerAmount = customerAmountInput;
474
+ partnerSplit = toCents(partnerAmountInput);
475
+ customerSplit = toCents(customerAmountInput);
476
+ } else if (hasLegacyFixedInputs) {
477
+ if (legacyPartnerSplitInput == null || legacyPartnerSplitInput < 0) throw new Error(`Commission rule ${index + 1}: For fixed commissions, both partner and customer values must be non-negative numbers`);
478
+ const legacyHasTotalValue = toNumber(r.totalCommission?.value) != null;
479
+ const resolvedLegacyCustomerSplit = legacyCustomerSplitInput ?? (legacyHasTotalValue ? 100 - legacyPartnerSplitInput : null);
480
+ if (resolvedLegacyCustomerSplit == null || resolvedLegacyCustomerSplit < 0) throw new Error(`Commission rule ${index + 1}: For fixed commissions, both partner and customer values must be non-negative numbers`);
481
+ partnerSplit = legacyPartnerSplitInput;
482
+ customerSplit = resolvedLegacyCustomerSplit;
483
+ partnerAmount = null;
484
+ customerAmount = null;
485
+ } else throw new Error(`Commission rule ${index + 1}: For fixed commissions, both partner and customer values must be provided`);
486
+ }
393
487
  const minOrderAmount = toNumber(r.minOrderAmount);
394
488
  if (minOrderAmount != null && minOrderAmount < 0) throw new Error(`Commission rule ${index + 1}: Minimum Order Amount must be a non-negative number`);
395
489
  return {
396
490
  ...rule,
397
491
  appliesTo: appliesTo === "categories" ? "segments" : appliesTo,
398
492
  totalCommission: {
399
- type: r.totalCommission.type,
400
- value: totalValue,
493
+ type,
494
+ value: type === "percentage" ? totalValue : null,
401
495
  maxAmount: maxAmount ?? null
402
496
  },
497
+ partnerPercent,
498
+ customerPercent,
499
+ partnerAmount,
500
+ customerAmount,
403
501
  partnerSplit,
404
502
  customerSplit,
405
503
  minOrderAmount: minOrderAmount ?? null
@@ -456,7 +554,7 @@ const createReferralProgramsCollection = (pluginConfig) => {
456
554
  {
457
555
  name: "products",
458
556
  type: "relationship",
459
- relationTo: "products",
557
+ relationTo: relationSlugs.productsSlug,
460
558
  hasMany: true,
461
559
  admin: {
462
560
  condition: (_, siblingData) => siblingData?.appliesTo === "products",
@@ -466,7 +564,7 @@ const createReferralProgramsCollection = (pluginConfig) => {
466
564
  {
467
565
  name: "categories",
468
566
  type: "relationship",
469
- relationTo: "categories",
567
+ relationTo: relationSlugs.categoriesSlug,
470
568
  hasMany: true,
471
569
  admin: {
472
570
  condition: (_, siblingData) => siblingData?.appliesTo === "segments",
@@ -476,7 +574,7 @@ const createReferralProgramsCollection = (pluginConfig) => {
476
574
  {
477
575
  name: "tags",
478
576
  type: "relationship",
479
- relationTo: "tags",
577
+ relationTo: relationSlugs.tagsSlug,
480
578
  hasMany: true,
481
579
  admin: {
482
580
  condition: (_, siblingData) => siblingData?.appliesTo === "segments",
@@ -486,7 +584,7 @@ const createReferralProgramsCollection = (pluginConfig) => {
486
584
  {
487
585
  name: "totalCommission",
488
586
  type: "group",
489
- admin: { description: "Total commission pool to split between partner and customer" },
587
+ admin: { description: "Total commission pool configuration" },
490
588
  fields: [
491
589
  {
492
590
  name: "type",
@@ -501,11 +599,12 @@ const createReferralProgramsCollection = (pluginConfig) => {
501
599
  {
502
600
  name: "value",
503
601
  type: "number",
602
+ min: 0,
603
+ max: 100,
504
604
  admin: {
505
605
  condition: ({ siblingData }) => siblingData?.type === "percentage",
506
- description: "Total commission value (shown for percentage rules; ignored when using fixed split amounts)"
507
- },
508
- min: 0
606
+ description: "Total commission percentage for this rule (0-100). Partner/Customer percentages split this 100-based bucket."
607
+ }
509
608
  },
510
609
  {
511
610
  name: "maxAmount",
@@ -516,30 +615,74 @@ const createReferralProgramsCollection = (pluginConfig) => {
516
615
  ]
517
616
  },
518
617
  {
519
- name: "partnerSplit",
618
+ name: "partnerPercent",
520
619
  type: "number",
521
- required: true,
522
620
  min: 0,
523
- admin: { description: "For percentage rules this is the percent that goes to the partner; when using fixed type it becomes the literal amount per item" }
621
+ max: 100,
622
+ admin: {
623
+ condition: ({ siblingData }) => siblingData?.totalCommission?.type === "percentage",
624
+ description: "Partner share in percent (0-100). Customer share is auto-calculated as 100 - Partner."
625
+ }
524
626
  },
525
627
  {
526
- name: "minOrderAmount",
628
+ name: "customerPercent",
527
629
  type: "number",
528
630
  min: 0,
529
- admin: { description: `Minimum cart subtotal required for this rule in ${defaultCurrency}. Leave empty for no minimum.` }
631
+ max: 100,
632
+ admin: {
633
+ readOnly: true,
634
+ condition: ({ siblingData }) => siblingData?.totalCommission?.type === "percentage",
635
+ description: "Auto-calculated customer share percentage."
636
+ },
637
+ hooks: { beforeValidate: [({ siblingData }) => {
638
+ if (!siblingData || siblingData.totalCommission?.type !== "percentage") return null;
639
+ const partner = toNumber(siblingData.partnerPercent) ?? toNumber(siblingData.partnerSplit) ?? 0;
640
+ if (partner < 0) return 100;
641
+ if (partner > 100) return 0;
642
+ return 100 - partner;
643
+ }] }
644
+ },
645
+ {
646
+ name: "partnerAmount",
647
+ type: "number",
648
+ min: 0,
649
+ admin: {
650
+ condition: ({ siblingData }) => siblingData?.totalCommission?.type === "fixed",
651
+ description: `Fixed partner commission amount per item in ${defaultCurrency}. Stored as cents internally.`
652
+ }
653
+ },
654
+ {
655
+ name: "customerAmount",
656
+ type: "number",
657
+ min: 0,
658
+ admin: {
659
+ condition: ({ siblingData }) => siblingData?.totalCommission?.type === "fixed",
660
+ description: `Fixed customer discount amount per item in ${defaultCurrency}. Stored as cents internally.`
661
+ }
662
+ },
663
+ {
664
+ name: "partnerSplit",
665
+ type: "number",
666
+ min: 0,
667
+ admin: {
668
+ hidden: true,
669
+ description: "Canonical storage field. Percentage mode: percent. Fixed mode: amount in cents."
670
+ }
530
671
  },
531
672
  {
532
673
  name: "customerSplit",
533
674
  type: "number",
534
675
  min: 0,
535
676
  admin: {
536
- condition: ({ siblingData }) => siblingData?.totalCommission?.type !== "fixed",
537
- description: "When using percentage rules this is auto-calculated; for fixed-type rules you may enter a literal amount"
538
- },
539
- hooks: {
540
- beforeValidate: [({ siblingData }) => deriveCustomerSplit(siblingData?.partnerSplit, siblingData?.totalCommission?.type)],
541
- beforeChange: [({ siblingData }) => deriveCustomerSplit(siblingData?.partnerSplit, siblingData?.totalCommission?.type)]
677
+ hidden: true,
678
+ description: "Canonical storage field. Percentage mode: percent. Fixed mode: amount in cents."
542
679
  }
680
+ },
681
+ {
682
+ name: "minOrderAmount",
683
+ type: "number",
684
+ min: 0,
685
+ admin: { description: `Minimum cart subtotal required for this rule in ${defaultCurrency}. Leave empty for no minimum.` }
543
686
  }
544
687
  ]
545
688
  }
@@ -547,7 +690,6 @@ const createReferralProgramsCollection = (pluginConfig) => {
547
690
  timestamps: true
548
691
  };
549
692
  };
550
-
551
693
  //#endregion
552
694
  //#region src/utilities/roundTo2.ts
553
695
  /**
@@ -556,12 +698,8 @@ const createReferralProgramsCollection = (pluginConfig) => {
556
698
  function roundTo2(value) {
557
699
  return Math.round(value * 100) / 100;
558
700
  }
559
-
560
- //#endregion
561
- //#region src/utilities/pricing.ts
562
- const DEFAULT_PRICE_CURRENCY = "AED";
563
701
  function normalizeCurrencyCode(currencyCode) {
564
- if (!currencyCode) return DEFAULT_PRICE_CURRENCY;
702
+ if (!currencyCode) return "AED";
565
703
  return currencyCode.toUpperCase();
566
704
  }
567
705
  function readNumberField(entity, key) {
@@ -572,7 +710,7 @@ function readNumberField(entity, key) {
572
710
  function getPriceFieldKey(currencyCode) {
573
711
  return `priceIn${normalizeCurrencyCode(currencyCode)}`;
574
712
  }
575
- function readMoneyField(entity, currencyCode, defaultCurrencyCode = DEFAULT_PRICE_CURRENCY) {
713
+ function readMoneyField(entity, currencyCode, defaultCurrencyCode = "AED") {
576
714
  if (!entity) return void 0;
577
715
  const primaryField = getPriceFieldKey(currencyCode);
578
716
  const primary = readNumberField(entity, primaryField);
@@ -584,16 +722,15 @@ function readMoneyField(entity, currencyCode, defaultCurrencyCode = DEFAULT_PRIC
584
722
  }
585
723
  return typeof entity.price === "number" ? entity.price : void 0;
586
724
  }
587
- function resolveMoneyField(entity, currencyCode, defaultCurrencyCode = DEFAULT_PRICE_CURRENCY) {
725
+ function resolveMoneyField(entity, currencyCode, defaultCurrencyCode = "AED") {
588
726
  return readMoneyField(entity, currencyCode, defaultCurrencyCode) ?? 0;
589
727
  }
590
- function getCartItemUnitPrice({ item, product, variant, currencyCode, defaultCurrencyCode = DEFAULT_PRICE_CURRENCY }) {
728
+ function getCartItemUnitPrice({ item, product, variant, currencyCode, defaultCurrencyCode = "AED" }) {
591
729
  if (typeof item?.price === "number") return item.price;
592
730
  if (typeof item?.unitPrice === "number") return item.unitPrice;
593
731
  if (variant) return resolveMoneyField(variant, currencyCode, defaultCurrencyCode);
594
732
  return resolveMoneyField(product, currencyCode, defaultCurrencyCode);
595
733
  }
596
-
597
734
  //#endregion
598
735
  //#region src/utilities/calculateValues.ts
599
736
  function calculateCouponDiscount({ coupon, cartTotal }) {
@@ -607,7 +744,7 @@ function calculateCouponDiscount({ coupon, cartTotal }) {
607
744
  }
608
745
  return roundTo2(discount);
609
746
  }
610
- function relationId(value) {
747
+ function relationId$5(value) {
611
748
  if (value == null) return null;
612
749
  if (typeof value === "string" || typeof value === "number") return value;
613
750
  if (typeof value === "object" && (typeof value.id === "string" || typeof value.id === "number")) return value.id;
@@ -616,7 +753,7 @@ function relationId(value) {
616
753
  const allowedCommissionTypesSet = (allowed) => new Set((allowed && allowed.length ? allowed : ["fixed", "percentage"]).map((v) => v));
617
754
  function normalizeIds(values) {
618
755
  if (!Array.isArray(values)) return [];
619
- return values.map(relationId).filter((v) => v != null);
756
+ return values.map(relationId$5).filter((v) => v != null);
620
757
  }
621
758
  function getRuleSplits(rule) {
622
759
  const partnerRaw = typeof rule.partnerSplit === "number" ? rule.partnerSplit : typeof rule.referrerSplit === "number" ? rule.referrerSplit : null;
@@ -671,7 +808,7 @@ function calculateItemRewardByRule({ rule, itemTotal, quantity, allowedTotalComm
671
808
  }
672
809
  function getItemCategoryIds(item) {
673
810
  const productCategories = Array.isArray(item?.product?.categories) ? normalizeIds(item.product.categories) : [];
674
- const singleCategory = relationId(item?.category ?? item?.product?.category);
811
+ const singleCategory = relationId$5(item?.category ?? item?.product?.category);
675
812
  return [...productCategories, ...singleCategory != null ? [singleCategory] : []];
676
813
  }
677
814
  function getItemTagIds(item) {
@@ -684,7 +821,7 @@ function selectBestRuleForItem({ rules, item, itemTotal, quantity, cartTotal, al
684
821
  if (typeof rule?.minOrderAmount === "number" && Number.isFinite(rule.minOrderAmount)) return cartTotal >= rule.minOrderAmount;
685
822
  return true;
686
823
  });
687
- const productId = relationId(item.product);
824
+ const productId = relationId$5(item.product);
688
825
  const itemCategoryIds = new Set(getItemCategoryIds(item));
689
826
  const itemTagIds = new Set(getItemTagIds(item));
690
827
  const candidates = [
@@ -779,66 +916,118 @@ function calculateCommissionAndDiscount({ cartItems, program, currencyCode = "AE
779
916
  customerDiscount: totalCustomerDiscount
780
917
  };
781
918
  }
782
-
783
919
  //#endregion
784
920
  //#region src/endpoints/applyCoupon.ts
785
- const globalDebugLogs = [];
786
- const getRelationId = (value) => {
921
+ function relationId$4(value) {
787
922
  if (value == null) return null;
788
923
  if (typeof value === "string" || typeof value === "number") return value;
789
924
  if (typeof value === "object" && (typeof value.id === "string" || typeof value.id === "number")) return value.id;
790
925
  return null;
791
- };
926
+ }
927
+ function readField$3(doc, field) {
928
+ if (!doc || typeof doc !== "object") return void 0;
929
+ return doc[field];
930
+ }
931
+ function writeField$1(doc, field, value) {
932
+ doc[field] = value;
933
+ }
934
+ function normalizeCode$1(value) {
935
+ return typeof value === "string" ? value.trim().toUpperCase() : "";
936
+ }
937
+ async function findByNormalizedCode$1({ payload, collection, normalizedCode }) {
938
+ const exactQuery = await payload.find({
939
+ collection,
940
+ where: { normalizedCode: { equals: normalizedCode } },
941
+ limit: 1
942
+ });
943
+ if (exactQuery?.docs?.[0]) return exactQuery.docs[0];
944
+ const lowerQuery = await payload.find({
945
+ collection,
946
+ where: { code: { equals: normalizedCode.toLowerCase() } },
947
+ limit: 1
948
+ });
949
+ if (lowerQuery?.docs?.[0]) return lowerQuery.docs[0];
950
+ const upperQuery = await payload.find({
951
+ collection,
952
+ where: { code: { equals: normalizedCode.toUpperCase() } },
953
+ limit: 1
954
+ });
955
+ if (upperQuery?.docs?.[0]) return upperQuery.docs[0];
956
+ return (await payload.find({
957
+ collection,
958
+ where: { code: { equals: normalizedCode } },
959
+ limit: 1
960
+ }))?.docs?.[0] ?? null;
961
+ }
792
962
  const applyCouponHandler = ({ pluginConfig }) => async (req) => {
793
- globalDebugLogs.length = 0;
794
963
  const { payload } = req;
795
- const { code: rawCode, cartID, customerEmail } = req.data || {};
796
- const code = typeof rawCode === "string" ? rawCode.trim() : rawCode;
797
- if (!code || !cartID) return Response.json({
964
+ const fields = pluginConfig.integration.fields;
965
+ const collections = pluginConfig.integration.collections;
966
+ const rawCode = req?.data?.code;
967
+ const cartID = req?.data?.cartID;
968
+ const customerEmail = req?.data?.customerEmail;
969
+ const normalizedCode = normalizeCode$1(rawCode);
970
+ if (!normalizedCode || !cartID) return Response.json({
798
971
  success: false,
799
972
  error: `${pluginConfig.enableReferrals ? "Referral code" : "Coupon code"} and cart ID are required`
800
973
  }, { status: 400 });
974
+ const allowCoupon = await Promise.resolve(pluginConfig.policies.canApplyCoupon({
975
+ req,
976
+ user: req?.user,
977
+ payload
978
+ }));
979
+ const allowReferral = await Promise.resolve(pluginConfig.policies.canApplyReferral({
980
+ req,
981
+ user: req?.user,
982
+ payload
983
+ }));
984
+ if (!allowCoupon && !(pluginConfig.enableReferrals && allowReferral)) return Response.json({
985
+ success: false,
986
+ error: "Forbidden"
987
+ }, { status: 403 });
801
988
  try {
802
- const cartQuery = await payload.findByID({
803
- collection: "carts",
989
+ const cart = await payload.findByID({
990
+ collection: collections.cartsSlug,
804
991
  id: cartID,
805
992
  depth: 2
806
993
  });
807
- if (!cartQuery) return Response.json({
994
+ if (!cart) return Response.json({
808
995
  success: false,
809
996
  error: "Cart not found"
810
997
  }, { status: 404 });
811
- if (pluginConfig.referralConfig.singleCodePerCart) {
812
- const hasExistingCoupon = cartQuery.appliedCoupon;
813
- const hasExistingReferral = cartQuery.appliedReferralCode;
814
- if (hasExistingCoupon || hasExistingReferral) return Response.json({
815
- success: false,
816
- error: "A code has already been applied to this cart. Only one code can be used per order."
817
- }, { status: 400 });
818
- }
819
- if (pluginConfig.enableReferrals) {
998
+ const cartAppliedCoupon = relationId$4(readField$3(cart, fields.cartAppliedCouponField));
999
+ const cartAppliedReferral = relationId$4(readField$3(cart, fields.cartAppliedReferralCodeField));
1000
+ if (pluginConfig.referralConfig.singleCodePerCart && (cartAppliedCoupon || cartAppliedReferral)) return Response.json({
1001
+ success: false,
1002
+ error: "A code has already been applied to this cart. Only one code can be used per order."
1003
+ }, { status: 400 });
1004
+ if (pluginConfig.enableReferrals && allowReferral) {
820
1005
  const referralResult = await handleReferralCode({
821
1006
  payload,
822
- code,
1007
+ cart,
823
1008
  cartID,
824
- cart: cartQuery,
825
- customerEmail,
1009
+ normalizedCode,
826
1010
  pluginConfig
827
1011
  });
828
- if (!referralResult.ok && referralResult.status === 404 && pluginConfig.referralConfig.allowBothSystems) return await handleCouponCode({
1012
+ if (!referralResult.ok && referralResult.status === 404 && pluginConfig.referralConfig.allowBothSystems && allowCoupon) return await handleCouponCode({
829
1013
  payload,
830
- code,
1014
+ cart,
831
1015
  cartID,
832
- cart: cartQuery,
1016
+ normalizedCode,
833
1017
  customerEmail,
834
1018
  pluginConfig
835
1019
  });
836
1020
  return referralResult;
837
- } else return await handleCouponCode({
1021
+ }
1022
+ if (!allowCoupon) return Response.json({
1023
+ success: false,
1024
+ error: "Forbidden"
1025
+ }, { status: 403 });
1026
+ return await handleCouponCode({
838
1027
  payload,
839
- code,
1028
+ cart,
840
1029
  cartID,
841
- cart: cartQuery,
1030
+ normalizedCode,
842
1031
  customerEmail,
843
1032
  pluginConfig
844
1033
  });
@@ -850,27 +1039,18 @@ const applyCouponHandler = ({ pluginConfig }) => async (req) => {
850
1039
  }, { status: 500 });
851
1040
  }
852
1041
  };
853
- async function handleCouponCode({ payload, code, cartID, cart, customerEmail, pluginConfig }) {
854
- let couponQuery = await payload.find({
1042
+ async function handleCouponCode({ payload, cart, cartID, normalizedCode, customerEmail, pluginConfig }) {
1043
+ const fields = pluginConfig.integration.fields;
1044
+ const resolvers = pluginConfig.integration.resolvers;
1045
+ const coupon = await findByNormalizedCode$1({
1046
+ payload,
855
1047
  collection: pluginConfig.collections.couponsSlug,
856
- where: { code: { equals: code } },
857
- limit: 1
1048
+ normalizedCode
858
1049
  });
859
- if (!couponQuery.docs.length) couponQuery = await payload.find({
860
- collection: pluginConfig.collections.couponsSlug,
861
- where: { code: { equals: code.toLowerCase() } },
862
- limit: 1
863
- });
864
- if (!couponQuery.docs.length) couponQuery = await payload.find({
865
- collection: pluginConfig.collections.couponsSlug,
866
- where: { code: { equals: code.toUpperCase() } },
867
- limit: 1
868
- });
869
- if (!couponQuery.docs.length) return Response.json({
1050
+ if (!coupon) return Response.json({
870
1051
  success: false,
871
1052
  error: "Invalid coupon code"
872
1053
  }, { status: 404 });
873
- const coupon = couponQuery.docs[0];
874
1054
  const now = /* @__PURE__ */ new Date();
875
1055
  const activeFrom = coupon.activeFrom ? new Date(coupon.activeFrom) : null;
876
1056
  const activeUntil = coupon.activeUntil ? new Date(coupon.activeUntil) : null;
@@ -892,13 +1072,12 @@ async function handleCouponCode({ payload, code, cartID, cart, customerEmail, pl
892
1072
  success: false,
893
1073
  error: "Customer email is required for this coupon."
894
1074
  }, { status: 400 });
895
- const { ordersSlug, orderCustomerEmailField, orderPaymentStatusField, orderPaidStatusValue } = pluginConfig.orderIntegration;
896
1075
  if ((await payload.find({
897
- collection: ordersSlug,
1076
+ collection: pluginConfig.orderIntegration.ordersSlug,
898
1077
  where: { and: [
899
- { appliedCoupon: { equals: coupon.id } },
900
- { [orderCustomerEmailField]: { equals: email } },
901
- { [orderPaymentStatusField]: { equals: orderPaidStatusValue } }
1078
+ { [fields.orderAppliedCouponField]: { equals: coupon.id } },
1079
+ { [pluginConfig.orderIntegration.orderCustomerEmailField]: { equals: email } },
1080
+ { [pluginConfig.orderIntegration.orderPaymentStatusField]: { equals: pluginConfig.orderIntegration.orderPaidStatusValue } }
902
1081
  ] },
903
1082
  limit: 0
904
1083
  })).totalDocs >= coupon.perCustomerLimit) return Response.json({
@@ -906,11 +1085,12 @@ async function handleCouponCode({ payload, code, cartID, cart, customerEmail, pl
906
1085
  error: "You have reached the maximum uses for this coupon."
907
1086
  }, { status: 400 });
908
1087
  }
909
- if (getRelationId(cart.appliedCoupon) === coupon.id) return Response.json({
1088
+ if (relationId$4(readField$3(cart, fields.cartAppliedCouponField)) === coupon.id) return Response.json({
910
1089
  success: false,
911
1090
  error: "Coupon already applied to this cart"
912
1091
  }, { status: 400 });
913
- const cartTotal = cart.subtotal || cart.total || 0;
1092
+ const cartSubtotal = Number(resolvers.getCartSubtotal(cart)) || 0;
1093
+ const cartTotal = Number(resolvers.getCartTotal(cart)) || cartSubtotal || 0;
914
1094
  if (coupon.minOrderValue && cartTotal < coupon.minOrderValue) return Response.json({
915
1095
  success: false,
916
1096
  error: `Minimum order value of ${coupon.minOrderValue} ${pluginConfig.defaultCurrency} required`
@@ -923,15 +1103,15 @@ async function handleCouponCode({ payload, code, cartID, cart, customerEmail, pl
923
1103
  coupon,
924
1104
  cartTotal
925
1105
  });
926
- const total = roundTo2(Math.max(0, cartTotal - discountAmount));
1106
+ const nextTotal = roundTo2(Math.max(0, cartTotal - discountAmount));
1107
+ const data = {};
1108
+ writeField$1(data, fields.cartAppliedCouponField, coupon.id);
1109
+ writeField$1(data, fields.cartDiscountAmountField, discountAmount);
1110
+ writeField$1(data, fields.cartTotalField, nextTotal);
927
1111
  await payload.update({
928
- collection: "carts",
1112
+ collection: pluginConfig.integration.collections.cartsSlug,
929
1113
  id: cartID,
930
- data: {
931
- appliedCoupon: coupon.id,
932
- discountAmount,
933
- total
934
- }
1114
+ data
935
1115
  });
936
1116
  return Response.json({
937
1117
  success: true,
@@ -942,34 +1122,21 @@ async function handleCouponCode({ payload, code, cartID, cart, customerEmail, pl
942
1122
  value: coupon.value
943
1123
  },
944
1124
  discount: discountAmount,
945
- currency: pluginConfig.defaultCurrency,
946
- debug: globalDebugLogs
1125
+ currency: pluginConfig.defaultCurrency
947
1126
  });
948
1127
  }
949
- async function handleReferralCode({ payload, code, cartID, cart, customerEmail: _customerEmail, pluginConfig }) {
950
- let referralQuery = await payload.find({
1128
+ async function handleReferralCode({ payload, cart, cartID, normalizedCode, pluginConfig }) {
1129
+ const fields = pluginConfig.integration.fields;
1130
+ const resolvers = pluginConfig.integration.resolvers;
1131
+ const referralCode = await findByNormalizedCode$1({
1132
+ payload,
951
1133
  collection: pluginConfig.collections.referralCodesSlug,
952
- where: { code: { equals: code } },
953
- limit: 1,
954
- depth: 1
1134
+ normalizedCode
955
1135
  });
956
- if (!referralQuery.docs.length) referralQuery = await payload.find({
957
- collection: pluginConfig.collections.referralCodesSlug,
958
- where: { code: { equals: code.toLowerCase() } },
959
- limit: 1,
960
- depth: 1
961
- });
962
- if (!referralQuery.docs.length) referralQuery = await payload.find({
963
- collection: pluginConfig.collections.referralCodesSlug,
964
- where: { code: { equals: code.toUpperCase() } },
965
- limit: 1,
966
- depth: 1
967
- });
968
- if (!referralQuery.docs.length) return Response.json({
1136
+ if (!referralCode) return Response.json({
969
1137
  success: false,
970
1138
  error: "Invalid referral code"
971
1139
  }, { status: 404 });
972
- const referralCode = referralQuery.docs[0];
973
1140
  if (!referralCode.isActive) return Response.json({
974
1141
  success: false,
975
1142
  error: "Referral code is not active"
@@ -982,7 +1149,7 @@ async function handleReferralCode({ payload, code, cartID, cart, customerEmail:
982
1149
  success: false,
983
1150
  error: "Referral code usage limit exceeded"
984
1151
  }, { status: 400 });
985
- const programId = typeof referralCode.program === "string" ? referralCode.program : referralCode.program?.id;
1152
+ const programId = typeof referralCode.program === "string" || typeof referralCode.program === "number" ? referralCode.program : referralCode.program?.id;
986
1153
  const program = await payload.findByID({
987
1154
  collection: pluginConfig.collections.referralProgramsSlug,
988
1155
  id: programId
@@ -991,11 +1158,12 @@ async function handleReferralCode({ payload, code, cartID, cart, customerEmail:
991
1158
  success: false,
992
1159
  error: "Referral program is not active"
993
1160
  }, { status: 400 });
994
- if (getRelationId(cart.appliedReferralCode) === referralCode.id) return Response.json({
1161
+ if (relationId$4(readField$3(cart, fields.cartAppliedReferralCodeField)) === referralCode.id) return Response.json({
995
1162
  success: false,
996
1163
  error: "Referral code already applied to this cart"
997
1164
  }, { status: 400 });
998
- const cartTotal = cart.subtotal || cart.total || 0;
1165
+ const cartItems = resolvers.getCartItems(cart);
1166
+ const cartTotal = Number(resolvers.getCartTotal(cart)) || Number(resolvers.getCartSubtotal(cart)) || 0;
999
1167
  const minOrderAmount = getProgramMinimumOrderAmount({
1000
1168
  program,
1001
1169
  allowedTotalCommissionTypes: pluginConfig.referralConfig.allowedTotalCommissionTypes
@@ -1005,7 +1173,7 @@ async function handleReferralCode({ payload, code, cartID, cart, customerEmail:
1005
1173
  error: `Minimum order value of ${minOrderAmount} ${pluginConfig.defaultCurrency} required for this referral program`
1006
1174
  }, { status: 400 });
1007
1175
  const { partnerCommission, customerDiscount } = calculateCommissionAndDiscount({
1008
- cartItems: cart.items || [],
1176
+ cartItems,
1009
1177
  program,
1010
1178
  currencyCode: pluginConfig.defaultCurrency,
1011
1179
  cartTotal,
@@ -1013,16 +1181,16 @@ async function handleReferralCode({ payload, code, cartID, cart, customerEmail:
1013
1181
  });
1014
1182
  const roundedPartnerCommission = roundTo2(partnerCommission);
1015
1183
  const roundedCustomerDiscount = roundTo2(customerDiscount);
1016
- const total = roundTo2(Math.max(0, cartTotal - roundedCustomerDiscount));
1184
+ const nextTotal = roundTo2(Math.max(0, cartTotal - roundedCustomerDiscount));
1185
+ const data = {};
1186
+ writeField$1(data, fields.cartAppliedReferralCodeField, referralCode.id);
1187
+ writeField$1(data, fields.cartPartnerCommissionField, roundedPartnerCommission);
1188
+ writeField$1(data, fields.cartCustomerDiscountField, roundedCustomerDiscount);
1189
+ writeField$1(data, fields.cartTotalField, nextTotal);
1017
1190
  await payload.update({
1018
- collection: "carts",
1191
+ collection: pluginConfig.integration.collections.cartsSlug,
1019
1192
  id: cartID,
1020
- data: {
1021
- appliedReferralCode: referralCode.id,
1022
- partnerCommission: roundedPartnerCommission,
1023
- customerDiscount: roundedCustomerDiscount,
1024
- total
1025
- }
1193
+ data
1026
1194
  });
1027
1195
  return Response.json({
1028
1196
  success: true,
@@ -1030,8 +1198,7 @@ async function handleReferralCode({ payload, code, cartID, cart, customerEmail:
1030
1198
  referralCode: { code: referralCode.code },
1031
1199
  partnerCommission: roundedPartnerCommission,
1032
1200
  customerDiscount: roundedCustomerDiscount,
1033
- currency: pluginConfig.defaultCurrency,
1034
- debug: globalDebugLogs
1201
+ currency: pluginConfig.defaultCurrency
1035
1202
  });
1036
1203
  }
1037
1204
  const applyCouponEndpoint = ({ pluginConfig }) => ({
@@ -1039,70 +1206,115 @@ const applyCouponEndpoint = ({ pluginConfig }) => ({
1039
1206
  method: "post",
1040
1207
  handler: applyCouponHandler({ pluginConfig })
1041
1208
  });
1042
-
1043
1209
  //#endregion
1044
1210
  //#region src/endpoints/partnerStats.ts
1211
+ function relationId$3(value) {
1212
+ if (value == null) return null;
1213
+ if (typeof value === "string" || typeof value === "number") return value;
1214
+ if (typeof value === "object" && (typeof value.id === "string" || typeof value.id === "number")) return value.id;
1215
+ return null;
1216
+ }
1217
+ function readField$2(doc, field) {
1218
+ if (!doc || typeof doc !== "object") return void 0;
1219
+ return doc[field];
1220
+ }
1221
+ function asNumber$1(value) {
1222
+ return typeof value === "number" && Number.isFinite(value) ? value : 0;
1223
+ }
1224
+ function asString(value) {
1225
+ return typeof value === "string" ? value : "";
1226
+ }
1227
+ function toStatsStatus(value) {
1228
+ if (value === "paid") return "paid";
1229
+ if (value === "cancelled") return "cancelled";
1230
+ return "pending";
1231
+ }
1045
1232
  const partnerStatsHandler = ({ pluginConfig }) => async (req) => {
1046
1233
  const { payload, user } = req;
1234
+ const fields = pluginConfig.integration.fields;
1235
+ const collections = pluginConfig.integration.collections;
1047
1236
  if (!user) return Response.json({
1048
1237
  success: false,
1049
1238
  error: "Authentication required"
1050
1239
  }, { status: 401 });
1051
1240
  const typedUser = user;
1241
+ const userID = pluginConfig.integration.resolvers.getUserID({
1242
+ req,
1243
+ user
1244
+ });
1245
+ if (userID == null) return Response.json({
1246
+ success: false,
1247
+ error: "Unable to resolve user identity"
1248
+ }, { status: 403 });
1052
1249
  const isPartner = isPartnerUser({
1053
1250
  user: typedUser,
1054
1251
  roleConfig: pluginConfig.roleConfig
1055
- }) || pluginConfig.access.isPartner?.({ req });
1252
+ }) || await Promise.resolve(pluginConfig.access.isPartner?.({ req }));
1056
1253
  const isAdmin = isAdminUser({
1057
1254
  user: typedUser,
1058
1255
  roleConfig: pluginConfig.roleConfig
1059
- }) || pluginConfig.access.isAdmin?.({ req });
1060
- if (!isPartner && !isAdmin) return Response.json({
1256
+ }) || await Promise.resolve(pluginConfig.access.isAdmin?.({ req }));
1257
+ if (!await Promise.resolve(pluginConfig.policies.canViewPartnerStats({
1258
+ req,
1259
+ user,
1260
+ payload,
1261
+ requestedPartnerID: userID
1262
+ })) && !isAdmin && !isPartner) return Response.json({
1061
1263
  success: false,
1062
1264
  error: "Partner access required"
1063
1265
  }, { status: 403 });
1064
1266
  try {
1065
- const referralCodes = (await payload.find({
1267
+ const referralCodesQuery = await payload.find({
1066
1268
  collection: pluginConfig.collections.referralCodesSlug,
1067
- where: { partner: { equals: typedUser.id } },
1269
+ where: { partner: { equals: userID } },
1068
1270
  limit: 100
1069
- })).docs;
1271
+ });
1272
+ const referralCodes = Array.isArray(referralCodesQuery?.docs) ? referralCodesQuery.docs : [];
1070
1273
  let totalEarnings = 0;
1071
1274
  let pendingEarnings = 0;
1072
1275
  let paidEarnings = 0;
1073
1276
  let totalReferrals = 0;
1074
1277
  let successfulReferrals = 0;
1075
1278
  const referralCodeData = referralCodes.map((code) => {
1076
- totalEarnings += code.totalEarnings || 0;
1077
- pendingEarnings += code.pendingEarnings || 0;
1078
- paidEarnings += code.paidEarnings || 0;
1079
- totalReferrals += code.usageCount || 0;
1080
- successfulReferrals += code.successfulReferralsCount || 0;
1279
+ totalEarnings += asNumber$1(code?.totalEarnings);
1280
+ pendingEarnings += asNumber$1(code?.pendingEarnings);
1281
+ paidEarnings += asNumber$1(code?.paidEarnings);
1282
+ totalReferrals += asNumber$1(code?.usageCount);
1283
+ successfulReferrals += asNumber$1(code?.successfulReferralsCount);
1081
1284
  return {
1082
- id: code.id,
1083
- code: code.code,
1084
- usageCount: code.usageCount || 0,
1085
- totalEarnings: code.totalEarnings || 0,
1086
- isActive: code.isActive
1285
+ id: String(code?.id ?? ""),
1286
+ code: asString(code?.code),
1287
+ usageCount: asNumber$1(code?.usageCount),
1288
+ totalEarnings: asNumber$1(code?.totalEarnings),
1289
+ isActive: Boolean(code?.isActive)
1087
1290
  };
1088
1291
  });
1089
1292
  const conversionRate = totalReferrals > 0 ? successfulReferrals / totalReferrals * 100 : 0;
1090
1293
  const recentReferrals = [];
1091
1294
  try {
1092
- const ordersQuery = await payload.find({
1093
- collection: "orders",
1094
- where: { appliedReferralCode: { in: referralCodes.map((c) => c.id) } },
1095
- limit: 10,
1096
- sort: "-createdAt"
1097
- });
1098
- for (const order of ordersQuery.docs) recentReferrals.push({
1099
- id: order.id,
1100
- code: referralCodes.find((c) => c.id === order.appliedReferralCode)?.code || "",
1101
- orderValue: order.total || 0,
1102
- commission: order.partnerCommission || 0,
1103
- date: order.createdAt,
1104
- status: order.paymentStatus === "paid" ? "paid" : "pending"
1105
- });
1295
+ const referralCodeIDs = referralCodes.map((c) => relationId$3(c?.id)).filter((id) => id != null);
1296
+ if (referralCodeIDs.length > 0) {
1297
+ const ordersQuery = await payload.find({
1298
+ collection: collections.ordersSlug,
1299
+ where: { [fields.orderAppliedReferralCodeField]: { in: referralCodeIDs } },
1300
+ limit: 10,
1301
+ sort: `-${fields.orderCreatedAtField}`
1302
+ });
1303
+ for (const order of ordersQuery?.docs || []) {
1304
+ const orderReferralID = relationId$3(readField$2(order, fields.orderAppliedReferralCodeField));
1305
+ const matchedCode = referralCodes.find((c) => relationId$3(c?.id) === orderReferralID);
1306
+ const paymentStatus = readField$2(order, fields.orderPaymentStatusField);
1307
+ const createdAt = readField$2(order, fields.orderCreatedAtField);
1308
+ recentReferrals.push({
1309
+ id: String(order?.id ?? ""),
1310
+ code: asString(matchedCode?.code),
1311
+ orderValue: asNumber$1(readField$2(order, fields.cartTotalField) ?? order?.total),
1312
+ commission: asNumber$1(readField$2(order, fields.orderPartnerCommissionField)),
1313
+ date: asString(createdAt),
1314
+ status: toStatsStatus(paymentStatus)
1315
+ });
1316
+ }
1317
+ }
1106
1318
  } catch {}
1107
1319
  const monthlyEarnings = [];
1108
1320
  const now = /* @__PURE__ */ new Date();
@@ -1120,18 +1332,19 @@ const partnerStatsHandler = ({ pluginConfig }) => async (req) => {
1120
1332
  let program = null;
1121
1333
  if (referralCodes.length > 0) {
1122
1334
  const firstCode = referralCodes[0];
1123
- if (firstCode.program) try {
1335
+ const programID = relationId$3(firstCode?.program);
1336
+ if (programID != null) try {
1124
1337
  const programData = await payload.findByID({
1125
1338
  collection: pluginConfig.collections.referralProgramsSlug,
1126
- id: typeof firstCode.program === "string" ? firstCode.program : firstCode.program.id
1339
+ id: programID
1127
1340
  });
1128
1341
  if (programData) {
1129
1342
  const typedProgram = programData;
1130
- const firstRule = typedProgram.commissionRules?.[0];
1131
- const partnerSplit = firstRule?.partnerSplit ?? firstRule?.referrerSplit ?? firstRule?.split?.partnerPercentage ?? 0;
1132
- const customerSplit = firstRule?.customerSplit ?? firstRule?.refereeSplit ?? firstRule?.split?.customerPercentage ?? 100 - partnerSplit;
1343
+ const firstRule = typedProgram?.commissionRules?.[0];
1344
+ const partnerSplit = asNumber$1(firstRule?.partnerSplit) || asNumber$1(firstRule?.referrerSplit) || asNumber$1(firstRule?.split?.partnerPercentage);
1345
+ const customerSplit = asNumber$1(firstRule?.customerSplit) || asNumber$1(firstRule?.refereeSplit) || asNumber$1(firstRule?.split?.customerPercentage) || Math.max(0, 100 - partnerSplit);
1133
1346
  program = {
1134
- name: typedProgram.name,
1347
+ name: asString(typedProgram?.name),
1135
1348
  commissionRate: partnerSplit,
1136
1349
  customerDiscount: customerSplit
1137
1350
  };
@@ -1170,27 +1383,81 @@ const partnerStatsEndpoint = ({ pluginConfig }) => ({
1170
1383
  method: "get",
1171
1384
  handler: partnerStatsHandler({ pluginConfig })
1172
1385
  });
1173
-
1174
1386
  //#endregion
1175
1387
  //#region src/endpoints/validateCoupon.ts
1388
+ function relationId$2(value) {
1389
+ if (value == null) return null;
1390
+ if (typeof value === "string" || typeof value === "number") return value;
1391
+ if (typeof value === "object" && (typeof value.id === "string" || typeof value.id === "number")) return value.id;
1392
+ return null;
1393
+ }
1394
+ function normalizeCode(value) {
1395
+ return typeof value === "string" ? value.trim().toUpperCase() : "";
1396
+ }
1397
+ async function findByNormalizedCode({ payload, collection, normalizedCode }) {
1398
+ const normalizedQuery = await payload.find({
1399
+ collection,
1400
+ where: { normalizedCode: { equals: normalizedCode } },
1401
+ limit: 1
1402
+ });
1403
+ if (normalizedQuery?.docs?.length) return normalizedQuery.docs[0];
1404
+ const lowerQuery = await payload.find({
1405
+ collection,
1406
+ where: { code: { equals: normalizedCode.toLowerCase() } },
1407
+ limit: 1
1408
+ });
1409
+ if (lowerQuery?.docs?.length) return lowerQuery.docs[0];
1410
+ const upperQuery = await payload.find({
1411
+ collection,
1412
+ where: { code: { equals: normalizedCode.toUpperCase() } },
1413
+ limit: 1
1414
+ });
1415
+ if (upperQuery?.docs?.length) return upperQuery.docs[0];
1416
+ return (await payload.find({
1417
+ collection,
1418
+ where: { code: { equals: normalizedCode } },
1419
+ limit: 1
1420
+ }))?.docs?.[0] ?? null;
1421
+ }
1176
1422
  const validateCouponHandler = ({ pluginConfig }) => async (req) => {
1177
1423
  const { payload } = req;
1178
- const { code: rawCode, cartValue, cartID, customerEmail } = req.data || {};
1179
- const code = typeof rawCode === "string" ? rawCode.trim() : rawCode;
1180
- if (!code) return Response.json({
1424
+ const rawCode = req?.data?.code;
1425
+ const cartValue = req?.data?.cartValue;
1426
+ const cartID = req?.data?.cartID;
1427
+ const customerEmail = req?.data?.customerEmail;
1428
+ const normalizedCode = normalizeCode(rawCode);
1429
+ if (!normalizedCode) return Response.json({
1181
1430
  success: false,
1182
1431
  error: "Code is required"
1183
1432
  }, { status: 400 });
1184
1433
  try {
1185
- if (pluginConfig.enableReferrals) return await validateReferralCode({
1186
- payload,
1187
- code,
1188
- cartID,
1189
- pluginConfig
1190
- });
1191
- else return await validateCouponCode$1({
1434
+ if (pluginConfig.enableReferrals) {
1435
+ if (!await Promise.resolve(pluginConfig.policies.canApplyReferral({
1436
+ req,
1437
+ user: req?.user,
1438
+ payload
1439
+ }))) return Response.json({
1440
+ success: false,
1441
+ error: "Forbidden"
1442
+ }, { status: 403 });
1443
+ return await validateReferralCode({
1444
+ payload,
1445
+ normalizedCode,
1446
+ cartID,
1447
+ pluginConfig
1448
+ });
1449
+ }
1450
+ if (!await Promise.resolve(pluginConfig.policies.canApplyCoupon({
1451
+ req,
1452
+ user: req?.user,
1453
+ payload
1454
+ }))) return Response.json({
1455
+ success: false,
1456
+ error: "Forbidden"
1457
+ }, { status: 403 });
1458
+ return await validateCouponCode$1({
1192
1459
  payload,
1193
- code,
1460
+ normalizedCode,
1194
1461
  cartValue,
1195
1462
  customerEmail,
1196
1463
  pluginConfig
@@ -1203,27 +1470,17 @@ const validateCouponHandler = ({ pluginConfig }) => async (req) => {
1203
1470
  }, { status: 500 });
1204
1471
  }
1205
1472
  };
1206
- async function validateCouponCode$1({ payload, code, cartValue, customerEmail, pluginConfig }) {
1207
- let coupon = await payload.find({
1473
+ async function validateCouponCode$1({ payload, normalizedCode, cartValue, customerEmail, pluginConfig }) {
1474
+ const fields = pluginConfig.integration.fields;
1475
+ const couponData = await findByNormalizedCode({
1476
+ payload,
1208
1477
  collection: pluginConfig.collections.couponsSlug,
1209
- where: { code: { equals: code } },
1210
- limit: 1
1478
+ normalizedCode
1211
1479
  });
1212
- if (!coupon.docs.length) coupon = await payload.find({
1213
- collection: pluginConfig.collections.couponsSlug,
1214
- where: { code: { equals: code.toLowerCase() } },
1215
- limit: 1
1216
- });
1217
- if (!coupon.docs.length) coupon = await payload.find({
1218
- collection: pluginConfig.collections.couponsSlug,
1219
- where: { code: { equals: code.toUpperCase() } },
1220
- limit: 1
1221
- });
1222
- if (!coupon.docs.length) return Response.json({
1480
+ if (!couponData) return Response.json({
1223
1481
  success: false,
1224
1482
  error: "Invalid coupon code"
1225
1483
  }, { status: 404 });
1226
- const couponData = coupon.docs[0];
1227
1484
  const now = /* @__PURE__ */ new Date();
1228
1485
  const activeFrom = couponData.activeFrom ? new Date(couponData.activeFrom) : null;
1229
1486
  const activeUntil = couponData.activeUntil ? new Date(couponData.activeUntil) : null;
@@ -1241,13 +1498,12 @@ async function validateCouponCode$1({ payload, code, cartValue, customerEmail, p
1241
1498
  }, { status: 400 });
1242
1499
  if (couponData.perCustomerLimit != null && couponData.perCustomerLimit > 0 && typeof customerEmail === "string" && customerEmail.trim().length > 0) {
1243
1500
  const email = customerEmail.trim();
1244
- const { ordersSlug, orderCustomerEmailField, orderPaymentStatusField, orderPaidStatusValue } = pluginConfig.orderIntegration;
1245
1501
  if ((await payload.find({
1246
- collection: ordersSlug,
1502
+ collection: pluginConfig.orderIntegration.ordersSlug,
1247
1503
  where: { and: [
1248
- { appliedCoupon: { equals: couponData.id } },
1249
- { [orderCustomerEmailField]: { equals: email } },
1250
- { [orderPaymentStatusField]: { equals: orderPaidStatusValue } }
1504
+ { [fields.orderAppliedCouponField]: { equals: couponData.id } },
1505
+ { [pluginConfig.orderIntegration.orderCustomerEmailField]: { equals: email } },
1506
+ { [pluginConfig.orderIntegration.orderPaymentStatusField]: { equals: pluginConfig.orderIntegration.orderPaidStatusValue } }
1251
1507
  ] },
1252
1508
  limit: 0
1253
1509
  })).totalDocs >= couponData.perCustomerLimit) return Response.json({
@@ -1289,27 +1545,18 @@ async function validateCouponCode$1({ payload, code, cartValue, customerEmail, p
1289
1545
  currency: pluginConfig.defaultCurrency
1290
1546
  });
1291
1547
  }
1292
- async function validateReferralCode({ payload, code, cartID, pluginConfig }) {
1293
- let referral = await payload.find({
1548
+ async function validateReferralCode({ payload, normalizedCode, cartID, pluginConfig }) {
1549
+ const collections = pluginConfig.integration.collections;
1550
+ const resolvers = pluginConfig.integration.resolvers;
1551
+ const referralData = await findByNormalizedCode({
1552
+ payload,
1294
1553
  collection: pluginConfig.collections.referralCodesSlug,
1295
- where: { code: { equals: code } },
1296
- limit: 1
1554
+ normalizedCode
1297
1555
  });
1298
- if (!referral.docs.length) referral = await payload.find({
1299
- collection: pluginConfig.collections.referralCodesSlug,
1300
- where: { code: { equals: code.toLowerCase() } },
1301
- limit: 1
1302
- });
1303
- if (!referral.docs.length) referral = await payload.find({
1304
- collection: pluginConfig.collections.referralCodesSlug,
1305
- where: { code: { equals: code.toUpperCase() } },
1306
- limit: 1
1307
- });
1308
- if (!referral.docs.length) return Response.json({
1556
+ if (!referralData) return Response.json({
1309
1557
  success: false,
1310
1558
  error: "Referral code not found"
1311
1559
  }, { status: 404 });
1312
- const referralData = referral.docs[0];
1313
1560
  if (!referralData.isActive) return Response.json({
1314
1561
  success: false,
1315
1562
  error: "Referral code is not active"
@@ -1322,7 +1569,11 @@ async function validateReferralCode({ payload, code, cartID, pluginConfig }) {
1322
1569
  success: false,
1323
1570
  error: "Referral code usage limit exceeded"
1324
1571
  }, { status: 400 });
1325
- const programId = typeof referralData.program === "string" ? referralData.program : referralData.program?.id;
1572
+ const programId = relationId$2(referralData.program);
1573
+ if (programId == null) return Response.json({
1574
+ success: false,
1575
+ error: "Referral program not found"
1576
+ }, { status: 404 });
1326
1577
  const program = await payload.findByID({
1327
1578
  collection: pluginConfig.collections.referralProgramsSlug,
1328
1579
  id: programId
@@ -1332,11 +1583,11 @@ async function validateReferralCode({ payload, code, cartID, pluginConfig }) {
1332
1583
  error: "Referral program is not active"
1333
1584
  }, { status: 400 });
1334
1585
  const cart = cartID ? await payload.findByID({
1335
- collection: "carts",
1586
+ collection: collections.cartsSlug,
1336
1587
  id: cartID,
1337
1588
  depth: 2
1338
1589
  }) : null;
1339
- const cartTotal = cart ? cart.subtotal || cart.total || 0 : 0;
1590
+ const cartTotal = cart ? Number(resolvers.getCartTotal(cart)) || Number(resolvers.getCartSubtotal(cart)) || 0 : 0;
1340
1591
  const minOrderAmount = getProgramMinimumOrderAmount({
1341
1592
  program,
1342
1593
  allowedTotalCommissionTypes: pluginConfig.referralConfig.allowedTotalCommissionTypes
@@ -1346,7 +1597,7 @@ async function validateReferralCode({ payload, code, cartID, pluginConfig }) {
1346
1597
  error: `Minimum order value of ${minOrderAmount} ${pluginConfig.defaultCurrency} required for this referral program`
1347
1598
  }, { status: 400 });
1348
1599
  const { partnerCommission, customerDiscount } = calculateCommissionAndDiscount({
1349
- cartItems: cart?.items || [],
1600
+ cartItems: cart ? resolvers.getCartItems(cart) : [],
1350
1601
  program,
1351
1602
  currencyCode: pluginConfig.defaultCurrency,
1352
1603
  cartTotal,
@@ -1371,152 +1622,428 @@ const validateCouponEndpoint = ({ pluginConfig }) => ({
1371
1622
  method: "post",
1372
1623
  handler: validateCouponHandler({ pluginConfig })
1373
1624
  });
1374
-
1375
1625
  //#endregion
1376
1626
  //#region src/hooks/recalculateCart.ts
1627
+ function relationId$1(value) {
1628
+ if (value == null) return null;
1629
+ if (typeof value === "string" || typeof value === "number") return value;
1630
+ if (typeof value === "object" && (typeof value.id === "string" || typeof value.id === "number")) return value.id;
1631
+ return null;
1632
+ }
1633
+ function readField$1(doc, field) {
1634
+ if (!doc || typeof doc !== "object") return void 0;
1635
+ return doc[field];
1636
+ }
1637
+ function writeField(doc, field, value) {
1638
+ doc[field] = value;
1639
+ }
1640
+ function clearCouponFields(target, fields) {
1641
+ writeField(target, fields.cartAppliedCouponField, null);
1642
+ writeField(target, fields.cartDiscountAmountField, 0);
1643
+ }
1644
+ function clearReferralFields(target, fields) {
1645
+ writeField(target, fields.cartAppliedReferralCodeField, null);
1646
+ writeField(target, fields.cartPartnerCommissionField, 0);
1647
+ writeField(target, fields.cartCustomerDiscountField, 0);
1648
+ }
1377
1649
  const recalculateCartHook = (pluginConfig) => async ({ data, req, originalDoc }) => {
1378
1650
  if (!req.payload) return data;
1379
- const effectiveItems = data.items || originalDoc?.items || [];
1380
- if (!effectiveItems.length) return {
1381
- ...data,
1382
- partnerCommission: 0,
1383
- customerDiscount: 0,
1384
- discountAmount: 0,
1385
- total: 0
1651
+ const integration = pluginConfig.integration || {};
1652
+ const collections = integration.collections || {
1653
+ cartsSlug: "carts",
1654
+ ordersSlug: "orders",
1655
+ productsSlug: "products",
1656
+ usersSlug: "users",
1657
+ categoriesSlug: "categories",
1658
+ tagsSlug: "tags"
1386
1659
  };
1387
- const appliedReferralCode = data.appliedReferralCode !== void 0 ? data.appliedReferralCode : originalDoc?.appliedReferralCode;
1388
- const appliedCoupon = data.appliedCoupon !== void 0 ? data.appliedCoupon : originalDoc?.appliedCoupon;
1389
- if (!appliedReferralCode && !appliedCoupon) {
1390
- if (data.appliedReferralCode === null || data.appliedCoupon === null) {
1391
- const fallbackSubtotal = typeof data.subtotal === "number" ? data.subtotal : typeof originalDoc?.subtotal === "number" ? originalDoc.subtotal : void 0;
1392
- return {
1393
- ...data,
1394
- partnerCommission: 0,
1395
- customerDiscount: 0,
1396
- discountAmount: 0,
1397
- total: fallbackSubtotal
1660
+ const fields = integration.fields || {
1661
+ cartItemsField: "items",
1662
+ cartSubtotalField: "subtotal",
1663
+ cartTotalField: "total",
1664
+ cartAppliedCouponField: "appliedCoupon",
1665
+ cartAppliedReferralCodeField: "appliedReferralCode",
1666
+ cartDiscountAmountField: "discountAmount",
1667
+ cartCustomerDiscountField: "customerDiscount",
1668
+ cartPartnerCommissionField: "partnerCommission",
1669
+ orderAppliedCouponField: "appliedCoupon",
1670
+ orderAppliedReferralCodeField: "appliedReferralCode",
1671
+ orderDiscountAmountField: "discountAmount",
1672
+ orderCustomerDiscountField: "customerDiscount",
1673
+ orderPartnerCommissionField: "partnerCommission",
1674
+ orderCustomerEmailField: "customerEmail",
1675
+ orderPaymentStatusField: "paymentStatus",
1676
+ orderCreatedAtField: "createdAt",
1677
+ productPriceField: "price",
1678
+ productCurrencyCodeField: "currencyCode"
1679
+ };
1680
+ const resolvers = integration.resolvers || {
1681
+ getUserID: ({ user }) => {
1682
+ if (!user || typeof user !== "object") return null;
1683
+ const id = user.id;
1684
+ if (typeof id === "string" || typeof id === "number") return id;
1685
+ return null;
1686
+ },
1687
+ getCartItems: (cart) => {
1688
+ if (!cart || typeof cart !== "object") return [];
1689
+ const value = cart[fields.cartItemsField];
1690
+ return Array.isArray(value) ? value : [];
1691
+ },
1692
+ getCartSubtotal: (cart) => {
1693
+ if (!cart || typeof cart !== "object") return 0;
1694
+ const value = cart[fields.cartSubtotalField];
1695
+ return typeof value === "number" ? value : 0;
1696
+ },
1697
+ getCartTotal: (cart) => {
1698
+ if (!cart || typeof cart !== "object") return 0;
1699
+ const value = cart[fields.cartTotalField];
1700
+ return typeof value === "number" ? value : 0;
1701
+ },
1702
+ isOrderPaid: (_order) => false,
1703
+ getProductUnitPrice: ({ item, product, variant, currencyCode }) => {
1704
+ if (item && typeof item === "object") {
1705
+ const itemPrice = item.price;
1706
+ if (typeof itemPrice === "number") return itemPrice;
1707
+ const unitPrice = item.unitPrice;
1708
+ if (typeof unitPrice === "number") return unitPrice;
1709
+ }
1710
+ const readPrice = (entity, code) => {
1711
+ if (!entity || typeof entity !== "object") return void 0;
1712
+ const map = entity;
1713
+ if (code && typeof code === "string") {
1714
+ const value = map[`priceIn${code.toUpperCase()}`];
1715
+ if (typeof value === "number") return value;
1716
+ }
1717
+ const base = map.price;
1718
+ return typeof base === "number" ? base : void 0;
1398
1719
  };
1720
+ return readPrice(variant, currencyCode) ?? readPrice(product, currencyCode) ?? 0;
1399
1721
  }
1400
- return data;
1401
- }
1402
- const getRelationID = (value) => {
1403
- if (value === null || value === void 0) return void 0;
1404
- if (typeof value === "object") return value.id;
1405
- if (typeof value === "string" || typeof value === "number") return value;
1406
1722
  };
1407
- const productIds = effectiveItems.map((item) => getRelationID(item.product)).filter((id) => id !== void 0);
1408
- if (!productIds.length) return data;
1409
- const productsQuery = await req.payload.find({
1410
- collection: "products",
1411
- where: { id: { in: productIds } },
1412
- limit: productIds.length
1413
- });
1414
- const productsMap = new Map(productsQuery.docs.map((p) => [String(p.id), p]));
1723
+ const mutableData = data || {};
1724
+ const original = originalDoc || {};
1725
+ const effectiveItems = readField$1(mutableData, fields.cartItemsField) ?? readField$1(original, fields.cartItemsField) ?? [];
1726
+ const effectiveAppliedReferral = readField$1(mutableData, fields.cartAppliedReferralCodeField) !== void 0 ? readField$1(mutableData, fields.cartAppliedReferralCodeField) : readField$1(original, fields.cartAppliedReferralCodeField);
1727
+ const effectiveAppliedCoupon = readField$1(mutableData, fields.cartAppliedCouponField) !== void 0 ? readField$1(mutableData, fields.cartAppliedCouponField) : readField$1(original, fields.cartAppliedCouponField);
1728
+ if (!Array.isArray(effectiveItems) || effectiveItems.length === 0) {
1729
+ clearReferralFields(mutableData, fields);
1730
+ clearCouponFields(mutableData, fields);
1731
+ writeField(mutableData, fields.cartTotalField, 0);
1732
+ return mutableData;
1733
+ }
1734
+ const getRelationID = (value) => relationId$1(value);
1735
+ const productIds = effectiveItems.map((item) => getRelationID(item?.product)).filter((id) => id != null);
1736
+ let productsMap = /* @__PURE__ */ new Map();
1737
+ if (productIds.length > 0) {
1738
+ const productsQuery = await req.payload.find({
1739
+ collection: collections.productsSlug,
1740
+ where: { id: { in: productIds } },
1741
+ limit: productIds.length
1742
+ });
1743
+ productsMap = new Map((productsQuery?.docs || []).map((p) => [String(p.id), p]));
1744
+ }
1415
1745
  let calculatedSubtotal = 0;
1416
1746
  const enrichedItems = effectiveItems.map((item) => {
1417
- const productId = getRelationID(item.product);
1418
- const product = productId !== void 0 ? productsMap.get(String(productId)) || {} : {};
1419
- const itemPrice = getCartItemUnitPrice({
1747
+ const pid = getRelationID(item?.product);
1748
+ const product = pid != null ? productsMap.get(String(pid)) || {} : {};
1749
+ const variant = typeof item?.variant === "object" ? item.variant : void 0;
1750
+ const unitPrice = Number(resolvers.getProductUnitPrice({
1420
1751
  item,
1421
1752
  product,
1422
- variant: typeof item.variant === "object" ? item.variant : void 0,
1753
+ variant,
1423
1754
  currencyCode: pluginConfig.defaultCurrency
1424
- });
1425
- calculatedSubtotal += itemPrice * (item.quantity ?? 1);
1755
+ }));
1756
+ const quantity = typeof item?.quantity === "number" && Number.isFinite(item.quantity) ? item.quantity : 1;
1757
+ const safeUnitPrice = Number.isFinite(unitPrice) ? unitPrice : 0;
1758
+ calculatedSubtotal += safeUnitPrice * quantity;
1426
1759
  return {
1427
1760
  ...item,
1428
1761
  product,
1429
- price: itemPrice
1762
+ price: safeUnitPrice,
1763
+ quantity
1430
1764
  };
1431
1765
  });
1432
- if (appliedReferralCode && pluginConfig.enableReferrals) {
1433
- const appliedReferralCodeID = getRelationID(appliedReferralCode);
1434
- if (appliedReferralCodeID === void 0) {
1435
- data.partnerCommission = 0;
1436
- data.customerDiscount = 0;
1437
- data.total = calculatedSubtotal;
1438
- return data;
1439
- }
1440
- const referralQuery = await req.payload.find({
1766
+ writeField(mutableData, fields.cartSubtotalField, roundTo2(calculatedSubtotal));
1767
+ let customerDiscount = 0;
1768
+ let couponDiscount = 0;
1769
+ const appliedReferralID = relationId$1(effectiveAppliedReferral);
1770
+ if (pluginConfig.enableReferrals && appliedReferralID != null) {
1771
+ const referralCode = (await req.payload.find({
1441
1772
  collection: pluginConfig.collections.referralCodesSlug,
1442
- where: { id: { equals: appliedReferralCodeID } },
1773
+ where: { id: { equals: appliedReferralID } },
1443
1774
  limit: 1,
1444
1775
  depth: 1
1445
- });
1446
- if (referralQuery.docs.length) {
1447
- const referralCode = referralQuery.docs[0];
1448
- const programId = typeof referralCode.program === "string" ? referralCode.program : referralCode.program?.id;
1449
- const program = typeof referralCode.program === "object" ? referralCode.program : programId ? await req.payload.findByID({
1776
+ }))?.docs?.[0];
1777
+ if (!referralCode || referralCode.isActive === false) clearReferralFields(mutableData, fields);
1778
+ else {
1779
+ const programId = relationId$1(referralCode.program);
1780
+ const program = typeof referralCode.program === "object" ? referralCode.program : programId != null ? await req.payload.findByID({
1450
1781
  collection: pluginConfig.collections.referralProgramsSlug,
1451
1782
  id: programId
1452
1783
  }) : null;
1453
- if (program) {
1784
+ if (!program || program.isActive === false) clearReferralFields(mutableData, fields);
1785
+ else {
1454
1786
  const minOrderAmount = getProgramMinimumOrderAmount({
1455
1787
  program,
1456
1788
  allowedTotalCommissionTypes: pluginConfig.referralConfig.allowedTotalCommissionTypes
1457
1789
  });
1458
- if (typeof minOrderAmount === "number" && calculatedSubtotal < minOrderAmount) {
1459
- data.appliedReferralCode = null;
1460
- data.partnerCommission = 0;
1461
- data.customerDiscount = 0;
1462
- data.total = calculatedSubtotal;
1463
- return data;
1790
+ if (typeof minOrderAmount === "number" && calculatedSubtotal < minOrderAmount) clearReferralFields(mutableData, fields);
1791
+ else {
1792
+ const result = calculateCommissionAndDiscount({
1793
+ cartItems: enrichedItems,
1794
+ program,
1795
+ currencyCode: pluginConfig.defaultCurrency,
1796
+ cartTotal: calculatedSubtotal,
1797
+ allowedTotalCommissionTypes: pluginConfig.referralConfig.allowedTotalCommissionTypes
1798
+ });
1799
+ const roundedPartnerCommission = roundTo2(result.partnerCommission);
1800
+ const roundedCustomerDiscount = roundTo2(Math.max(0, result.customerDiscount));
1801
+ writeField(mutableData, fields.cartPartnerCommissionField, roundedPartnerCommission);
1802
+ writeField(mutableData, fields.cartCustomerDiscountField, roundedCustomerDiscount);
1803
+ customerDiscount = roundedCustomerDiscount;
1464
1804
  }
1465
- const { partnerCommission, customerDiscount } = calculateCommissionAndDiscount({
1466
- cartItems: enrichedItems,
1467
- program,
1468
- currencyCode: pluginConfig.defaultCurrency,
1469
- cartTotal: calculatedSubtotal,
1470
- allowedTotalCommissionTypes: pluginConfig.referralConfig.allowedTotalCommissionTypes
1471
- });
1472
- const roundedCustomerDiscount = roundTo2(customerDiscount);
1473
- data.partnerCommission = roundTo2(partnerCommission);
1474
- data.customerDiscount = roundedCustomerDiscount;
1475
- data.total = Math.max(0, calculatedSubtotal - roundedCustomerDiscount);
1476
- } else {
1477
- data.appliedReferralCode = null;
1478
- data.partnerCommission = 0;
1479
- data.customerDiscount = 0;
1480
- data.total = calculatedSubtotal;
1481
1805
  }
1482
1806
  }
1483
- }
1484
- if (appliedCoupon && (!appliedReferralCode || pluginConfig.referralConfig.allowBothSystems)) {
1485
- const appliedCouponID = getRelationID(appliedCoupon);
1486
- if (appliedCouponID === void 0) return data;
1487
- const couponQuery = await req.payload.find({
1807
+ } else if (readField$1(mutableData, fields.cartAppliedReferralCodeField) === null) clearReferralFields(mutableData, fields);
1808
+ const appliedCouponID = relationId$1(effectiveAppliedCoupon);
1809
+ const canUseCouponWithReferral = !pluginConfig.enableReferrals || pluginConfig.referralConfig.allowBothSystems || relationId$1(readField$1(mutableData, fields.cartAppliedReferralCodeField)) == null;
1810
+ if (appliedCouponID != null && canUseCouponWithReferral) {
1811
+ const coupon = (await req.payload.find({
1488
1812
  collection: pluginConfig.collections.couponsSlug,
1489
1813
  where: { id: { equals: appliedCouponID } },
1490
1814
  limit: 1
1815
+ }))?.docs?.[0];
1816
+ if (!coupon) clearCouponFields(mutableData, fields);
1817
+ else {
1818
+ const now = /* @__PURE__ */ new Date();
1819
+ const activeFrom = coupon.activeFrom ? new Date(coupon.activeFrom) : null;
1820
+ const activeUntil = coupon.activeUntil ? new Date(coupon.activeUntil) : null;
1821
+ const isValidDate = (!activeFrom || now >= activeFrom) && (!activeUntil || now <= activeUntil);
1822
+ const underUsage = !coupon.usageLimit || Number(coupon.usageCount || 0) < Number(coupon.usageLimit || 0);
1823
+ if (!isValidDate || !underUsage) clearCouponFields(mutableData, fields);
1824
+ else {
1825
+ couponDiscount = roundTo2(calculateCouponDiscount({
1826
+ coupon,
1827
+ cartTotal: calculatedSubtotal
1828
+ }));
1829
+ writeField(mutableData, fields.cartDiscountAmountField, couponDiscount);
1830
+ }
1831
+ }
1832
+ } else if (readField$1(mutableData, fields.cartAppliedCouponField) === null) {
1833
+ clearCouponFields(mutableData, fields);
1834
+ writeField(mutableData, fields.cartCustomerDiscountField, 0);
1835
+ writeField(mutableData, fields.cartPartnerCommissionField, 0);
1836
+ writeField(mutableData, fields.cartTotalField, roundTo2(Number(resolvers.getCartSubtotal(mutableData)) || calculatedSubtotal));
1837
+ return mutableData;
1838
+ }
1839
+ const nextTotal = roundTo2(Math.max(0, calculatedSubtotal - customerDiscount - couponDiscount));
1840
+ writeField(mutableData, fields.cartTotalField, nextTotal);
1841
+ return mutableData;
1842
+ };
1843
+ //#endregion
1844
+ //#region src/utilities/recordCouponUsageForOrder.ts
1845
+ const USAGE_MARKER_FIELD = "couponUsageRecordedAt";
1846
+ function relationId(value) {
1847
+ if (value == null) return null;
1848
+ if (typeof value === "string" || typeof value === "number") return value;
1849
+ if (typeof value === "object" && (typeof value.id === "string" || typeof value.id === "number")) return value.id;
1850
+ return null;
1851
+ }
1852
+ function asNumber(value) {
1853
+ return typeof value === "number" && Number.isFinite(value) ? value : 0;
1854
+ }
1855
+ function readField(doc, field) {
1856
+ if (!doc || typeof doc !== "object") return void 0;
1857
+ return doc[field];
1858
+ }
1859
+ async function markOrderUsageRecorded({ payload, pluginConfig, orderID }) {
1860
+ const ordersSlug = pluginConfig.integration.collections.ordersSlug;
1861
+ const latestOrder = await payload.findByID({
1862
+ collection: ordersSlug,
1863
+ id: orderID,
1864
+ depth: 0
1865
+ });
1866
+ if (!latestOrder) return false;
1867
+ if (Boolean(readField(latestOrder, USAGE_MARKER_FIELD))) return false;
1868
+ await payload.update({
1869
+ collection: ordersSlug,
1870
+ id: orderID,
1871
+ data: { [USAGE_MARKER_FIELD]: (/* @__PURE__ */ new Date()).toISOString() }
1872
+ });
1873
+ return true;
1874
+ }
1875
+ /**
1876
+ * Record coupon and referral usage when an order is placed successfully.
1877
+ * This function is idempotent and integration-field aware.
1878
+ *
1879
+ * Behavior:
1880
+ * - Uses configured order field names from `pluginConfig.integration.fields`
1881
+ * - Uses configured paid-order resolver (`integration.resolvers.isOrderPaid`)
1882
+ * - Marks the order with `couponUsageRecordedAt` before mutating counters to avoid duplicate counting
1883
+ * - If marker already exists, returns `alreadyRecorded: true` and performs no increments
1884
+ */
1885
+ async function recordCouponUsageForOrder(payload, order, pluginConfig) {
1886
+ const result = {
1887
+ recordedCoupon: false,
1888
+ recordedReferral: false,
1889
+ alreadyRecorded: false
1890
+ };
1891
+ const orderID = order.id;
1892
+ if (orderID == null) return result;
1893
+ if (!await Promise.resolve(pluginConfig.integration.resolvers.isOrderPaid(order))) return result;
1894
+ if (!await Promise.resolve(pluginConfig.policies.canRecordOrderUsage({
1895
+ req: {},
1896
+ user: void 0,
1897
+ payload,
1898
+ order
1899
+ }))) return result;
1900
+ if (!await markOrderUsageRecorded({
1901
+ payload,
1902
+ pluginConfig,
1903
+ orderID
1904
+ })) {
1905
+ result.alreadyRecorded = true;
1906
+ return result;
1907
+ }
1908
+ const fields = pluginConfig.integration.fields;
1909
+ const couponField = fields.orderAppliedCouponField;
1910
+ const referralField = fields.orderAppliedReferralCodeField;
1911
+ const partnerCommissionField = fields.orderPartnerCommissionField;
1912
+ const couponId = relationId(readField(order, couponField));
1913
+ const referralCodeId = relationId(readField(order, referralField));
1914
+ const commission = asNumber(readField(order, partnerCommissionField));
1915
+ if (couponId) {
1916
+ const coupon = await payload.findByID({
1917
+ collection: pluginConfig.collections.couponsSlug,
1918
+ id: couponId,
1919
+ depth: 0
1920
+ });
1921
+ if (coupon) {
1922
+ const currentUsage = asNumber(coupon.usageCount);
1923
+ await payload.update({
1924
+ collection: pluginConfig.collections.couponsSlug,
1925
+ id: couponId,
1926
+ data: { usageCount: currentUsage + 1 }
1927
+ });
1928
+ result.recordedCoupon = true;
1929
+ }
1930
+ }
1931
+ if (referralCodeId) {
1932
+ const referralCode = await payload.findByID({
1933
+ collection: pluginConfig.collections.referralCodesSlug,
1934
+ id: referralCodeId,
1935
+ depth: 0
1491
1936
  });
1492
- if (couponQuery.docs.length) {
1493
- const coupon = couponQuery.docs[0];
1494
- const discountAmount = calculateCouponDiscount({
1495
- coupon,
1496
- cartTotal: calculatedSubtotal
1937
+ if (referralCode) {
1938
+ const rc = referralCode;
1939
+ const currentTotal = asNumber(rc.totalEarnings);
1940
+ const currentPending = asNumber(rc.pendingEarnings);
1941
+ const currentUsage = asNumber(rc.usageCount);
1942
+ const currentSuccessful = asNumber(rc.successfulReferralsCount);
1943
+ await payload.update({
1944
+ collection: pluginConfig.collections.referralCodesSlug,
1945
+ id: referralCodeId,
1946
+ data: {
1947
+ usageCount: currentUsage + 1,
1948
+ successfulReferralsCount: currentSuccessful + 1,
1949
+ totalEarnings: currentTotal + commission,
1950
+ pendingEarnings: currentPending + commission
1951
+ }
1497
1952
  });
1498
- data.discountAmount = discountAmount;
1499
- const currentDiscount = data.customerDiscount || 0;
1500
- data.total = Math.max(0, calculatedSubtotal - currentDiscount - discountAmount);
1953
+ result.recordedReferral = true;
1501
1954
  }
1502
1955
  }
1503
- return data;
1504
- };
1505
-
1956
+ return result;
1957
+ }
1506
1958
  //#endregion
1507
1959
  //#region src/utilities/sanitizePluginConfig.ts
1960
+ const toCleanStringArray = (value) => Array.isArray(value) ? value.filter((item) => typeof item === "string").map((item) => item.trim()).filter(Boolean) : [];
1961
+ const toBoolean = (value, fallback) => {
1962
+ if (typeof value === "boolean") return value;
1963
+ if (typeof value === "number") {
1964
+ if (value === 1) return true;
1965
+ if (value === 0) return false;
1966
+ }
1967
+ if (typeof value === "string") {
1968
+ const normalized = value.trim().toLowerCase();
1969
+ if (normalized === "true" || normalized === "1" || normalized === "yes" || normalized === "on") return true;
1970
+ if (normalized === "false" || normalized === "0" || normalized === "no" || normalized === "off") return false;
1971
+ }
1972
+ return fallback;
1973
+ };
1508
1974
  const sanitizePluginConfig = ({ pluginConfig }) => {
1509
1975
  const roleConfig = {
1510
- 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"],
1511
- 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"],
1512
- 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"],
1976
+ roleFieldPaths: toCleanStringArray(pluginConfig?.roleConfig?.roleFieldPaths).length > 0 ? toCleanStringArray(pluginConfig?.roleConfig?.roleFieldPaths) : ["role", "roles"],
1977
+ adminRoleValues: toCleanStringArray(pluginConfig?.roleConfig?.adminRoleValues).length > 0 ? toCleanStringArray(pluginConfig?.roleConfig?.adminRoleValues) : ["admin"],
1978
+ partnerRoleValues: toCleanStringArray(pluginConfig?.roleConfig?.partnerRoleValues).length > 0 ? toCleanStringArray(pluginConfig?.roleConfig?.partnerRoleValues) : ["partner"],
1513
1979
  customRoleResolver: typeof pluginConfig?.roleConfig?.customRoleResolver === "function" ? pluginConfig.roleConfig.customRoleResolver : void 0
1514
1980
  };
1515
1981
  const normalizedAllowedTotalCommissionTypes = Array.isArray(pluginConfig?.referralConfig?.allowedTotalCommissionTypes) ? [...new Set(pluginConfig.referralConfig.allowedTotalCommissionTypes.filter((value) => value === "fixed" || value === "percentage"))] : [];
1982
+ const integrationCollections = {
1983
+ cartsSlug: typeof pluginConfig?.integration?.collections?.cartsSlug === "string" && pluginConfig.integration.collections.cartsSlug.trim().length > 0 ? pluginConfig.integration.collections.cartsSlug.trim() : "carts",
1984
+ ordersSlug: typeof pluginConfig?.integration?.collections?.ordersSlug === "string" && pluginConfig.integration.collections.ordersSlug.trim().length > 0 ? pluginConfig.integration.collections.ordersSlug.trim() : "orders",
1985
+ productsSlug: typeof pluginConfig?.integration?.collections?.productsSlug === "string" && pluginConfig.integration.collections.productsSlug.trim().length > 0 ? pluginConfig.integration.collections.productsSlug.trim() : "products",
1986
+ usersSlug: typeof pluginConfig?.integration?.collections?.usersSlug === "string" && pluginConfig.integration.collections.usersSlug.trim().length > 0 ? pluginConfig.integration.collections.usersSlug.trim() : "users",
1987
+ categoriesSlug: typeof pluginConfig?.integration?.collections?.categoriesSlug === "string" && pluginConfig.integration.collections.categoriesSlug.trim().length > 0 ? pluginConfig.integration.collections.categoriesSlug.trim() : "categories",
1988
+ tagsSlug: typeof pluginConfig?.integration?.collections?.tagsSlug === "string" && pluginConfig.integration.collections.tagsSlug.trim().length > 0 ? pluginConfig.integration.collections.tagsSlug.trim() : "tags"
1989
+ };
1990
+ const integrationFields = {
1991
+ cartItemsField: pluginConfig?.integration?.fields?.cartItemsField?.trim() || "items",
1992
+ cartSubtotalField: pluginConfig?.integration?.fields?.cartSubtotalField?.trim() || "subtotal",
1993
+ cartTotalField: pluginConfig?.integration?.fields?.cartTotalField?.trim() || "total",
1994
+ cartAppliedCouponField: pluginConfig?.integration?.fields?.cartAppliedCouponField?.trim() || "appliedCoupon",
1995
+ cartAppliedReferralCodeField: pluginConfig?.integration?.fields?.cartAppliedReferralCodeField?.trim() || "appliedReferralCode",
1996
+ cartDiscountAmountField: pluginConfig?.integration?.fields?.cartDiscountAmountField?.trim() || "discountAmount",
1997
+ cartCustomerDiscountField: pluginConfig?.integration?.fields?.cartCustomerDiscountField?.trim() || "customerDiscount",
1998
+ cartPartnerCommissionField: pluginConfig?.integration?.fields?.cartPartnerCommissionField?.trim() || "partnerCommission",
1999
+ orderAppliedCouponField: pluginConfig?.integration?.fields?.orderAppliedCouponField?.trim() || "appliedCoupon",
2000
+ orderAppliedReferralCodeField: pluginConfig?.integration?.fields?.orderAppliedReferralCodeField?.trim() || "appliedReferralCode",
2001
+ orderDiscountAmountField: pluginConfig?.integration?.fields?.orderDiscountAmountField?.trim() || "discountAmount",
2002
+ orderCustomerDiscountField: pluginConfig?.integration?.fields?.orderCustomerDiscountField?.trim() || "customerDiscount",
2003
+ orderPartnerCommissionField: pluginConfig?.integration?.fields?.orderPartnerCommissionField?.trim() || "partnerCommission",
2004
+ orderCustomerEmailField: pluginConfig?.integration?.fields?.orderCustomerEmailField?.trim() || pluginConfig?.orderIntegration?.orderCustomerEmailField?.trim() || "customerEmail",
2005
+ orderPaymentStatusField: pluginConfig?.integration?.fields?.orderPaymentStatusField?.trim() || pluginConfig?.orderIntegration?.orderPaymentStatusField?.trim() || "paymentStatus",
2006
+ orderCreatedAtField: pluginConfig?.integration?.fields?.orderCreatedAtField?.trim() || "createdAt",
2007
+ productPriceField: pluginConfig?.integration?.fields?.productPriceField?.trim() || "price",
2008
+ productCurrencyCodeField: pluginConfig?.integration?.fields?.productCurrencyCodeField?.trim() || "currencyCode"
2009
+ };
2010
+ const integrationResolvers = {
2011
+ getUserID: pluginConfig?.integration?.resolvers?.getUserID || (({ user }) => {
2012
+ if (!user || typeof user !== "object") return null;
2013
+ const id = user.id;
2014
+ if (typeof id === "string" || typeof id === "number") return id;
2015
+ return null;
2016
+ }),
2017
+ getCartItems: pluginConfig?.integration?.resolvers?.getCartItems || ((cart) => {
2018
+ if (!cart || typeof cart !== "object") return [];
2019
+ const value = cart[integrationFields.cartItemsField];
2020
+ return Array.isArray(value) ? value : [];
2021
+ }),
2022
+ getCartSubtotal: pluginConfig?.integration?.resolvers?.getCartSubtotal || ((cart) => {
2023
+ if (!cart || typeof cart !== "object") return 0;
2024
+ const value = cart[integrationFields.cartSubtotalField];
2025
+ return typeof value === "number" ? value : 0;
2026
+ }),
2027
+ getCartTotal: pluginConfig?.integration?.resolvers?.getCartTotal || ((cart) => {
2028
+ if (!cart || typeof cart !== "object") return 0;
2029
+ const value = cart[integrationFields.cartTotalField];
2030
+ return typeof value === "number" ? value : 0;
2031
+ }),
2032
+ isOrderPaid: pluginConfig?.integration?.resolvers?.isOrderPaid || ((order) => {
2033
+ if (!order || typeof order !== "object") return false;
2034
+ return order[integrationFields.orderPaymentStatusField] === (pluginConfig?.orderIntegration?.orderPaidStatusValue ?? "paid");
2035
+ }),
2036
+ getProductUnitPrice: pluginConfig?.integration?.resolvers?.getProductUnitPrice || ((args) => getCartItemUnitPrice({
2037
+ item: args.item ?? null,
2038
+ product: args.product ?? null,
2039
+ variant: args.variant ?? null,
2040
+ currencyCode: args.currencyCode || "USD"
2041
+ }))
2042
+ };
1516
2043
  return {
1517
- enabled: !(pluginConfig?.enabled === false || typeof pluginConfig?.enabled === "string" && pluginConfig.enabled === "false"),
1518
- enableReferrals: !!pluginConfig?.enableReferrals && (typeof pluginConfig?.enableReferrals !== "string" || pluginConfig.enableReferrals !== "false"),
1519
- allowStackWithOtherCoupons: !!pluginConfig?.allowStackWithOtherCoupons && (typeof pluginConfig?.allowStackWithOtherCoupons !== "string" || pluginConfig.allowStackWithOtherCoupons !== "false"),
2044
+ enabled: toBoolean(pluginConfig?.enabled, true),
2045
+ enableReferrals: toBoolean(pluginConfig?.enableReferrals, false),
2046
+ allowStackWithOtherCoupons: toBoolean(pluginConfig?.allowStackWithOtherCoupons, false),
1520
2047
  defaultCurrency: typeof pluginConfig?.defaultCurrency === "string" && pluginConfig.defaultCurrency.length > 0 && pluginConfig.defaultCurrency.length <= 3 ? pluginConfig.defaultCurrency : "USD",
1521
2048
  collections: {
1522
2049
  couponsSlug: typeof pluginConfig?.collections?.couponsSlug === "string" && pluginConfig.collections.couponsSlug.trim().length > 0 && pluginConfig.collections.couponsSlug.length <= 100 ? pluginConfig.collections.couponsSlug : "coupons",
@@ -1525,10 +2052,10 @@ const sanitizePluginConfig = ({ pluginConfig }) => {
1525
2052
  referralPartnersSlug: typeof pluginConfig?.collections?.referralPartnersSlug === "string" && pluginConfig.collections.referralPartnersSlug.trim().length > 0 && pluginConfig.collections.referralPartnersSlug.length <= 100 ? pluginConfig.collections.referralPartnersSlug : "referral-partners"
1526
2053
  },
1527
2054
  endpoints: {
1528
- applyCoupon: typeof pluginConfig?.endpoints?.applyCoupon === "string" && pluginConfig.endpoints.applyCoupon.trim().length > 0 ? pluginConfig.endpoints.applyCoupon : "/coupons/apply",
1529
- validateCoupon: typeof pluginConfig?.endpoints?.validateCoupon === "string" && pluginConfig.endpoints.validateCoupon.trim().length > 0 ? pluginConfig.endpoints.validateCoupon : "/coupons/validate",
1530
- partnerStats: typeof pluginConfig?.endpoints?.partnerStats === "string" && pluginConfig.endpoints.partnerStats.trim().length > 0 ? pluginConfig.endpoints.partnerStats : "/referrals/partner-stats",
1531
- recordOrderUsage: typeof pluginConfig?.endpoints?.recordOrderUsage === "string" && pluginConfig.endpoints.recordOrderUsage.trim().length > 0 ? pluginConfig.endpoints.recordOrderUsage : "/coupons/record-order-usage"
2055
+ applyCoupon: typeof pluginConfig?.endpoints?.applyCoupon === "string" && pluginConfig.endpoints.applyCoupon.trim().length > 0 ? pluginConfig.endpoints.applyCoupon.trim() : "/coupons/apply",
2056
+ validateCoupon: typeof pluginConfig?.endpoints?.validateCoupon === "string" && pluginConfig.endpoints.validateCoupon.trim().length > 0 ? pluginConfig.endpoints.validateCoupon.trim() : "/coupons/validate",
2057
+ partnerStats: typeof pluginConfig?.endpoints?.partnerStats === "string" && pluginConfig.endpoints.partnerStats.trim().length > 0 ? pluginConfig.endpoints.partnerStats.trim() : "/referrals/partner-stats",
2058
+ recordOrderUsage: typeof pluginConfig?.endpoints?.recordOrderUsage === "string" && pluginConfig.endpoints.recordOrderUsage.trim().length > 0 ? pluginConfig.endpoints.recordOrderUsage.trim() : "/coupons/record-order-usage"
1532
2059
  },
1533
2060
  autoIntegrate: pluginConfig?.autoIntegrate !== false,
1534
2061
  access: {
@@ -1543,6 +2070,23 @@ const sanitizePluginConfig = ({ pluginConfig }) => {
1543
2070
  roleConfig
1544
2071
  })
1545
2072
  },
2073
+ policies: {
2074
+ canApplyCoupon: typeof pluginConfig?.policies?.canApplyCoupon === "function" ? pluginConfig.policies.canApplyCoupon : () => true,
2075
+ canApplyReferral: typeof pluginConfig?.policies?.canApplyReferral === "function" ? pluginConfig.policies.canApplyReferral : () => true,
2076
+ canViewPartnerStats: typeof pluginConfig?.policies?.canViewPartnerStats === "function" ? pluginConfig.policies.canViewPartnerStats : ({ req }) => isPartnerUser({
2077
+ user: req?.user,
2078
+ roleConfig
2079
+ }) || isAdminUser({
2080
+ user: req?.user,
2081
+ roleConfig
2082
+ }),
2083
+ canRecordOrderUsage: typeof pluginConfig?.policies?.canRecordOrderUsage === "function" ? pluginConfig.policies.canRecordOrderUsage : () => true
2084
+ },
2085
+ integration: {
2086
+ collections: integrationCollections,
2087
+ fields: integrationFields,
2088
+ resolvers: integrationResolvers
2089
+ },
1546
2090
  referralConfig: {
1547
2091
  allowBothSystems: pluginConfig?.referralConfig?.allowBothSystems ?? false,
1548
2092
  singleCodePerCart: pluginConfig?.referralConfig?.singleCodePerCart ?? true,
@@ -1562,17 +2106,84 @@ const sanitizePluginConfig = ({ pluginConfig }) => {
1562
2106
  showCommissionBreakdown: pluginConfig?.partnerDashboard?.showCommissionBreakdown ?? true
1563
2107
  },
1564
2108
  orderIntegration: {
1565
- ordersSlug: typeof pluginConfig?.orderIntegration?.ordersSlug === "string" && pluginConfig.orderIntegration.ordersSlug.trim().length > 0 ? pluginConfig.orderIntegration.ordersSlug : "orders",
1566
- orderCustomerEmailField: typeof pluginConfig?.orderIntegration?.orderCustomerEmailField === "string" && pluginConfig.orderIntegration.orderCustomerEmailField.trim().length > 0 ? pluginConfig.orderIntegration.orderCustomerEmailField : "customerEmail",
1567
- orderPaymentStatusField: typeof pluginConfig?.orderIntegration?.orderPaymentStatusField === "string" && pluginConfig.orderIntegration.orderPaymentStatusField.trim().length > 0 ? pluginConfig.orderIntegration.orderPaymentStatusField : "paymentStatus",
2109
+ ordersSlug: typeof pluginConfig?.orderIntegration?.ordersSlug === "string" && pluginConfig.orderIntegration.ordersSlug.trim().length > 0 ? pluginConfig.orderIntegration.ordersSlug : integrationCollections.ordersSlug,
2110
+ orderCustomerEmailField: integrationFields.orderCustomerEmailField,
2111
+ orderPaymentStatusField: integrationFields.orderPaymentStatusField,
1568
2112
  orderPaidStatusValue: typeof pluginConfig?.orderIntegration?.orderPaidStatusValue === "string" ? pluginConfig.orderIntegration.orderPaidStatusValue : "paid"
1569
2113
  },
1570
2114
  roleConfig
1571
2115
  };
1572
2116
  };
1573
-
1574
2117
  //#endregion
1575
2118
  //#region src/plugin.ts
2119
+ const RECALCULATE_HOOK_KEY = "__payloadEcommerceCouponRecalculateHook__";
2120
+ const asArray = (value) => Array.isArray(value) ? value : [];
2121
+ const hasNamedField = (collection, fieldName) => asArray(collection.fields).some((f) => f?.name === fieldName);
2122
+ const addFieldsToCollection = (config, targetSlug, newFields) => {
2123
+ const collections = asArray(config.collections);
2124
+ const idx = collections.findIndex((c) => c.slug === targetSlug);
2125
+ if (idx === -1) return;
2126
+ const collection = collections[idx];
2127
+ collection.fields = asArray(collection.fields);
2128
+ for (const field of newFields) {
2129
+ const name = typeof field.name === "string" ? field.name : "";
2130
+ if (!name) continue;
2131
+ if (!hasNamedField(collection, name)) collection.fields.push(field);
2132
+ }
2133
+ collections[idx] = collection;
2134
+ config.collections = collections;
2135
+ };
2136
+ const markHook = (fn) => {
2137
+ fn[RECALCULATE_HOOK_KEY] = true;
2138
+ return fn;
2139
+ };
2140
+ const hasMarkedHook = (hook) => Boolean(hook && typeof hook === "function" && hook[RECALCULATE_HOOK_KEY]);
2141
+ const createRecordOrderUsageEndpoint = ({ pluginConfig }) => ({
2142
+ path: pluginConfig.endpoints.recordOrderUsage,
2143
+ method: "post",
2144
+ handler: async (req) => {
2145
+ try {
2146
+ const payload = req?.payload;
2147
+ const orderId = req?.data?.orderId ?? req?.json?.orderId;
2148
+ if (!payload) return Response.json({
2149
+ success: false,
2150
+ error: "Payload instance is required"
2151
+ }, { status: 500 });
2152
+ if (!orderId) return Response.json({
2153
+ success: false,
2154
+ error: "orderId is required"
2155
+ }, { status: 400 });
2156
+ if (!await Promise.resolve(pluginConfig.policies.canRecordOrderUsage({
2157
+ req,
2158
+ user: req?.user,
2159
+ payload,
2160
+ order: { id: orderId }
2161
+ }))) return Response.json({
2162
+ success: false,
2163
+ error: "Forbidden"
2164
+ }, { status: 403 });
2165
+ const order = await payload.findByID({
2166
+ collection: pluginConfig.integration.collections.ordersSlug,
2167
+ id: orderId
2168
+ });
2169
+ if (!order) return Response.json({
2170
+ success: false,
2171
+ error: "Order not found"
2172
+ }, { status: 404 });
2173
+ const result = await recordCouponUsageForOrder(payload, order, pluginConfig);
2174
+ return Response.json({
2175
+ success: true,
2176
+ result
2177
+ });
2178
+ } catch (error) {
2179
+ console.error("record-order-usage endpoint error:", error);
2180
+ return Response.json({
2181
+ success: false,
2182
+ error: "Internal server error"
2183
+ }, { status: 500 });
2184
+ }
2185
+ }
2186
+ });
1576
2187
  const payloadEcommerceCouponPlugin = (pluginOptions = {}) => async (incomingConfig) => {
1577
2188
  const pluginConfig = sanitizePluginConfig({ pluginConfig: pluginOptions });
1578
2189
  if (!pluginConfig.enabled) return incomingConfig || {};
@@ -1580,7 +2191,8 @@ const payloadEcommerceCouponPlugin = (pluginOptions = {}) => async (incomingConf
1580
2191
  collections: [],
1581
2192
  endpoints: []
1582
2193
  };
1583
- if (!incomingConfig.collections) incomingConfig.collections = [];
2194
+ incomingConfig.collections = asArray(incomingConfig.collections);
2195
+ incomingConfig.endpoints = asArray(incomingConfig.endpoints);
1584
2196
  const collectionsToAdd = [];
1585
2197
  if (pluginConfig.enableReferrals) {
1586
2198
  let referralProgramsCollection = createReferralProgramsCollection(pluginConfig);
@@ -1598,64 +2210,59 @@ const payloadEcommerceCouponPlugin = (pluginOptions = {}) => async (incomingConf
1598
2210
  if (pluginOptions.collections?.couponsCollectionOverride) couponsCollection = await pluginOptions.collections.couponsCollectionOverride({ defaultCollection: couponsCollection });
1599
2211
  collectionsToAdd.push(couponsCollection);
1600
2212
  }
1601
- const existingSlugs = new Set(incomingConfig.collections.map((c) => c.slug));
1602
- const collectionsToAddFiltered = collectionsToAdd.filter((c) => !existingSlugs.has(c.slug));
1603
- incomingConfig.collections = [...incomingConfig.collections, ...collectionsToAddFiltered];
1604
- if (!incomingConfig.endpoints) incomingConfig.endpoints = [];
1605
- incomingConfig.endpoints = [
1606
- ...incomingConfig.endpoints,
1607
- validateCouponEndpoint({ pluginConfig }),
1608
- applyCouponEndpoint({ pluginConfig })
1609
- ];
1610
- if (pluginConfig.enableReferrals) incomingConfig.endpoints.push(partnerStatsEndpoint({ pluginConfig }));
2213
+ const existingSlugs = new Set(asArray(incomingConfig.collections).map((c) => c.slug));
2214
+ const toAppend = collectionsToAdd.filter((c) => !existingSlugs.has(c.slug));
2215
+ incomingConfig.collections = [...asArray(incomingConfig.collections), ...toAppend];
2216
+ const endpointPaths = new Set(asArray(incomingConfig.endpoints).map((e) => `${e?.method || "get"}:${e?.path || ""}`));
2217
+ const maybePushEndpoint = (endpoint) => {
2218
+ const key = `${endpoint?.method || "get"}:${endpoint?.path || ""}`;
2219
+ if (!endpointPaths.has(key)) {
2220
+ endpointPaths.add(key);
2221
+ incomingConfig.endpoints.push(endpoint);
2222
+ }
2223
+ };
2224
+ maybePushEndpoint(validateCouponEndpoint({ pluginConfig }));
2225
+ maybePushEndpoint(applyCouponEndpoint({ pluginConfig }));
2226
+ if (pluginOptions.endpoints?.recordOrderUsage) maybePushEndpoint(createRecordOrderUsageEndpoint({ pluginConfig }));
2227
+ if (pluginConfig.enableReferrals) maybePushEndpoint(partnerStatsEndpoint({ pluginConfig }));
1611
2228
  if (pluginConfig.autoIntegrate) {
1612
- incomingConfig.collections = incomingConfig.collections || [];
1613
- const allSlugs = new Set(incomingConfig.collections.map((c) => c.slug));
1614
- const addFieldsToCollection = (targetSlug, newFields) => {
1615
- const idx = incomingConfig.collections.findIndex((c) => c.slug === targetSlug);
1616
- if (idx === -1) return;
1617
- const collection = incomingConfig.collections[idx];
1618
- collection.fields = collection.fields || [];
1619
- const existingFieldNames = new Set(collection.fields.map((f) => f.name));
1620
- for (const f of newFields) if (!existingFieldNames.has(f.name)) collection.fields.push(f);
1621
- incomingConfig.collections[idx] = collection;
1622
- };
2229
+ const allSlugs = new Set(asArray(incomingConfig.collections).map((c) => c.slug));
2230
+ const cartsSlug = pluginConfig.integration.collections.cartsSlug;
2231
+ const ordersSlug = pluginConfig.integration.collections.ordersSlug;
2232
+ const { cartAppliedReferralCodeField, cartPartnerCommissionField, cartCustomerDiscountField, cartAppliedCouponField, cartDiscountAmountField, orderAppliedReferralCodeField, orderPartnerCommissionField, orderCustomerDiscountField, orderAppliedCouponField, orderDiscountAmountField } = pluginConfig.integration.fields;
1623
2233
  if (pluginConfig.enableReferrals && allSlugs.has(pluginConfig.collections.referralCodesSlug)) {
1624
2234
  const cartReferralFields = [
1625
2235
  {
1626
- name: "appliedReferralCode",
2236
+ name: cartAppliedReferralCodeField,
1627
2237
  type: "relationship",
1628
2238
  relationTo: pluginConfig.collections.referralCodesSlug,
1629
2239
  admin: { description: "Referral code applied to this cart" }
1630
2240
  },
1631
2241
  {
1632
- name: "partnerCommission",
2242
+ name: cartPartnerCommissionField,
1633
2243
  type: "number",
1634
2244
  admin: { description: "Partner commission amount for this cart" }
1635
2245
  },
1636
2246
  {
1637
- name: "customerDiscount",
2247
+ name: cartCustomerDiscountField,
1638
2248
  type: "number",
1639
2249
  admin: { description: "Customer discount amount for this cart" }
1640
2250
  }
1641
2251
  ];
1642
- if (pluginConfig.referralConfig.allowBothSystems && allSlugs.has(pluginConfig.collections.couponsSlug)) {
1643
- cartReferralFields.push({
1644
- name: "appliedCoupon",
1645
- type: "relationship",
1646
- relationTo: pluginConfig.collections.couponsSlug,
1647
- admin: { description: "Coupon applied to this cart" }
1648
- });
1649
- cartReferralFields.push({
1650
- name: "discountAmount",
1651
- type: "number",
1652
- admin: { description: "Discount amount from coupon" }
1653
- });
1654
- }
1655
- addFieldsToCollection("carts", cartReferralFields);
2252
+ if (pluginConfig.referralConfig.allowBothSystems && allSlugs.has(pluginConfig.collections.couponsSlug)) cartReferralFields.push({
2253
+ name: cartAppliedCouponField,
2254
+ type: "relationship",
2255
+ relationTo: pluginConfig.collections.couponsSlug,
2256
+ admin: { description: "Coupon applied to this cart" }
2257
+ }, {
2258
+ name: cartDiscountAmountField,
2259
+ type: "number",
2260
+ admin: { description: "Discount amount from coupon" }
2261
+ });
2262
+ addFieldsToCollection(incomingConfig, cartsSlug, cartReferralFields);
1656
2263
  const orderReferralFields = [
1657
2264
  {
1658
- name: "appliedReferralCode",
2265
+ name: orderAppliedReferralCodeField,
1659
2266
  type: "relationship",
1660
2267
  relationTo: pluginConfig.collections.referralCodesSlug,
1661
2268
  admin: {
@@ -1664,7 +2271,7 @@ const payloadEcommerceCouponPlugin = (pluginOptions = {}) => async (incomingConf
1664
2271
  }
1665
2272
  },
1666
2273
  {
1667
- name: "partnerCommission",
2274
+ name: orderPartnerCommissionField,
1668
2275
  type: "number",
1669
2276
  admin: {
1670
2277
  description: "Partner commission amount for this order",
@@ -1672,7 +2279,7 @@ const payloadEcommerceCouponPlugin = (pluginOptions = {}) => async (incomingConf
1672
2279
  }
1673
2280
  },
1674
2281
  {
1675
- name: "customerDiscount",
2282
+ name: orderCustomerDiscountField,
1676
2283
  type: "number",
1677
2284
  admin: {
1678
2285
  description: "Customer discount amount for this order",
@@ -1680,39 +2287,36 @@ const payloadEcommerceCouponPlugin = (pluginOptions = {}) => async (incomingConf
1680
2287
  }
1681
2288
  }
1682
2289
  ];
1683
- if (pluginConfig.referralConfig.allowBothSystems && allSlugs.has(pluginConfig.collections.couponsSlug)) {
1684
- orderReferralFields.push({
1685
- name: "appliedCoupon",
1686
- type: "relationship",
1687
- relationTo: pluginConfig.collections.couponsSlug,
1688
- admin: {
1689
- description: "Coupon applied to this order",
1690
- readOnly: true
1691
- }
1692
- });
1693
- orderReferralFields.push({
1694
- name: "discountAmount",
1695
- type: "number",
1696
- admin: {
1697
- description: "Discount amount from coupon",
1698
- readOnly: true
1699
- }
1700
- });
1701
- }
1702
- addFieldsToCollection("orders", orderReferralFields);
2290
+ if (pluginConfig.referralConfig.allowBothSystems && allSlugs.has(pluginConfig.collections.couponsSlug)) orderReferralFields.push({
2291
+ name: orderAppliedCouponField,
2292
+ type: "relationship",
2293
+ relationTo: pluginConfig.collections.couponsSlug,
2294
+ admin: {
2295
+ description: "Coupon applied to this order",
2296
+ readOnly: true
2297
+ }
2298
+ }, {
2299
+ name: orderDiscountAmountField,
2300
+ type: "number",
2301
+ admin: {
2302
+ description: "Discount amount from coupon",
2303
+ readOnly: true
2304
+ }
2305
+ });
2306
+ addFieldsToCollection(incomingConfig, ordersSlug, orderReferralFields);
1703
2307
  } else if (!pluginConfig.enableReferrals && allSlugs.has(pluginConfig.collections.couponsSlug)) {
1704
- addFieldsToCollection("carts", [{
1705
- name: "appliedCoupon",
2308
+ const cartCouponFields = [{
2309
+ name: cartAppliedCouponField,
1706
2310
  type: "relationship",
1707
2311
  relationTo: pluginConfig.collections.couponsSlug,
1708
2312
  admin: { description: "Coupon applied to this cart" }
1709
2313
  }, {
1710
- name: "discountAmount",
2314
+ name: cartDiscountAmountField,
1711
2315
  type: "number",
1712
2316
  admin: { description: "Discount amount from coupon" }
1713
- }]);
1714
- addFieldsToCollection("orders", [{
1715
- name: "appliedCoupon",
2317
+ }];
2318
+ const orderCouponFields = [{
2319
+ name: orderAppliedCouponField,
1716
2320
  type: "relationship",
1717
2321
  relationTo: pluginConfig.collections.couponsSlug,
1718
2322
  admin: {
@@ -1720,43 +2324,75 @@ const payloadEcommerceCouponPlugin = (pluginOptions = {}) => async (incomingConf
1720
2324
  readOnly: true
1721
2325
  }
1722
2326
  }, {
1723
- name: "discountAmount",
2327
+ name: orderDiscountAmountField,
1724
2328
  type: "number",
1725
2329
  admin: {
1726
2330
  description: "Discount amount from coupon",
1727
2331
  readOnly: true
1728
2332
  }
1729
- }]);
2333
+ }];
2334
+ addFieldsToCollection(incomingConfig, cartsSlug, cartCouponFields);
2335
+ addFieldsToCollection(incomingConfig, ordersSlug, orderCouponFields);
1730
2336
  }
1731
2337
  }
1732
- const cartIndex = incomingConfig.collections.findIndex((c) => c.slug === "carts");
2338
+ const cartsSlug = pluginConfig.integration.collections.cartsSlug;
2339
+ const cartIndex = asArray(incomingConfig.collections).findIndex((c) => c.slug === cartsSlug);
1733
2340
  if (cartIndex > -1) {
1734
2341
  const collection = incomingConfig.collections[cartIndex];
1735
- collection.hooks = {
1736
- ...collection.hooks,
1737
- beforeChange: [...collection.hooks?.beforeChange || [], recalculateCartHook(pluginConfig)]
1738
- };
2342
+ const beforeChangeHooks = asArray(collection.hooks?.beforeChange);
2343
+ if (!beforeChangeHooks.some((h) => hasMarkedHook(h))) {
2344
+ const hook = markHook(recalculateCartHook(pluginConfig));
2345
+ collection.hooks = {
2346
+ ...collection.hooks || {},
2347
+ beforeChange: [...beforeChangeHooks, hook]
2348
+ };
2349
+ }
1739
2350
  incomingConfig.collections[cartIndex] = collection;
1740
2351
  }
1741
2352
  return incomingConfig;
1742
2353
  };
1743
-
1744
2354
  //#endregion
1745
2355
  //#region src/client/hooks.ts
2356
+ const DEFAULT_ENDPOINTS = {
2357
+ applyCoupon: "/api/coupons/apply",
2358
+ validateCoupon: "/api/coupons/validate",
2359
+ partnerStats: "/api/referrals/partner-stats"
2360
+ };
2361
+ function normalizePath(path) {
2362
+ if (!path) return "";
2363
+ return path.startsWith("/") ? path : `/${path}`;
2364
+ }
2365
+ function withBaseURL(path, baseURL) {
2366
+ if (!baseURL) return path;
2367
+ return `${baseURL.endsWith("/") ? baseURL.slice(0, -1) : baseURL}${normalizePath(path)}`;
2368
+ }
2369
+ function resolveEndpoints(input) {
2370
+ if (typeof input === "string") return {
2371
+ ...DEFAULT_ENDPOINTS,
2372
+ partnerStats: input
2373
+ };
2374
+ const baseURL = input?.baseURL;
2375
+ return {
2376
+ applyCoupon: withBaseURL(input?.applyCoupon || DEFAULT_ENDPOINTS.applyCoupon, baseURL),
2377
+ validateCoupon: withBaseURL(input?.validateCoupon || DEFAULT_ENDPOINTS.validateCoupon, baseURL),
2378
+ partnerStats: withBaseURL(input?.partnerStats || DEFAULT_ENDPOINTS.partnerStats, baseURL)
2379
+ };
2380
+ }
1746
2381
  /**
1747
- * Apply a coupon code to a cart
1748
- * @param options - Coupon code, cart ID, and customer email
1749
- * @returns Response with success status, discount amount, and coupon details
2382
+ * Apply a coupon/referral code to a cart
2383
+ * @param options - Code, cart ID, and optional customerEmail
2384
+ * @param endpointConfig - Optional endpoint override config
1750
2385
  */
1751
- async function useCouponCode(options) {
2386
+ async function useCouponCode(options, endpointConfig) {
1752
2387
  const { code, cartID, customerEmail } = options;
1753
2388
  if (!code) return {
1754
2389
  success: false,
1755
2390
  message: "Coupon code is required",
1756
2391
  error: "Code is missing"
1757
2392
  };
2393
+ const endpoints = resolveEndpoints(endpointConfig);
1758
2394
  try {
1759
- const response = await fetch("/api/coupons/apply", {
2395
+ const response = await fetch(endpoints.applyCoupon, {
1760
2396
  method: "POST",
1761
2397
  headers: { "Content-Type": "application/json" },
1762
2398
  body: JSON.stringify({
@@ -1774,8 +2410,8 @@ async function useCouponCode(options) {
1774
2410
  const couponData = data.coupon;
1775
2411
  const referralData = data.referralCode;
1776
2412
  return {
1777
- success: data.success,
1778
- message: data.message,
2413
+ success: Boolean(data.success),
2414
+ message: data.message || "Code applied",
1779
2415
  discount: data.discount || data.customerDiscount,
1780
2416
  partnerCommission: data.partnerCommission,
1781
2417
  customerDiscount: data.customerDiscount,
@@ -1796,25 +2432,29 @@ async function useCouponCode(options) {
1796
2432
  }
1797
2433
  }
1798
2434
  /**
1799
- * Validate a coupon code without applying it
1800
- * @param code - Coupon code to validate
1801
- * @param cartValue - Optional cart value in smallest currency unit
1802
- * @returns Response with validation result and coupon details
2435
+ * Validate a coupon/referral code without applying it
2436
+ * @param code - Code to validate
2437
+ * @param cartValue - Optional cart value
2438
+ * @param cartID - Optional cart ID
2439
+ * @param customerEmail - Optional customer email (for per-customer limits)
2440
+ * @param endpointConfig - Optional endpoint override config
1803
2441
  */
1804
- async function validateCouponCode(code, cartValue, cartID) {
2442
+ async function validateCouponCode(code, cartValue, cartID, customerEmail, endpointConfig) {
1805
2443
  if (!code) return {
1806
2444
  success: false,
1807
2445
  message: "Code required",
1808
2446
  error: "Code missing"
1809
2447
  };
2448
+ const endpoints = resolveEndpoints(endpointConfig);
1810
2449
  try {
1811
- const response = await fetch("/api/coupons/validate", {
2450
+ const response = await fetch(endpoints.validateCoupon, {
1812
2451
  method: "POST",
1813
2452
  headers: { "Content-Type": "application/json" },
1814
2453
  body: JSON.stringify({
1815
2454
  code,
1816
2455
  cartValue,
1817
- cartID
2456
+ cartID,
2457
+ customerEmail
1818
2458
  })
1819
2459
  });
1820
2460
  const data = await response.json();
@@ -1826,8 +2466,8 @@ async function validateCouponCode(code, cartValue, cartID) {
1826
2466
  const couponData = data.coupon;
1827
2467
  const referralData = data.referralCode;
1828
2468
  return {
1829
- success: data.success,
1830
- message: data.message,
2469
+ success: Boolean(data.success),
2470
+ message: data.message || "Code is valid",
1831
2471
  coupon: couponData ? {
1832
2472
  code: couponData.code || "",
1833
2473
  type: couponData.type || "percentage",
@@ -1850,12 +2490,12 @@ async function validateCouponCode(code, cartValue, cartID) {
1850
2490
  }
1851
2491
  /**
1852
2492
  * Fetch partner dashboard statistics
1853
- * @param apiEndpoint - Optional custom API endpoint (default: /api/referrals/partner-stats)
1854
- * @returns Response with partner stats, referral codes, and program info
2493
+ * @param endpointConfig - Optional endpoint override config
1855
2494
  */
1856
- async function usePartnerStats(apiEndpoint = "/api/referrals/partner-stats") {
2495
+ async function usePartnerStats(endpointConfig) {
2496
+ const endpoints = resolveEndpoints(endpointConfig);
1857
2497
  try {
1858
- const response = await fetch(apiEndpoint, {
2498
+ const response = await fetch(endpoints.partnerStats, {
1859
2499
  method: "GET",
1860
2500
  headers: { "Content-Type": "application/json" },
1861
2501
  credentials: "include"
@@ -1866,7 +2506,7 @@ async function usePartnerStats(apiEndpoint = "/api/referrals/partner-stats") {
1866
2506
  error: data.error || "Failed to fetch partner stats"
1867
2507
  };
1868
2508
  return {
1869
- success: data.success,
2509
+ success: Boolean(data.success),
1870
2510
  data: data.data,
1871
2511
  currency: data.currency
1872
2512
  };
@@ -1877,7 +2517,6 @@ async function usePartnerStats(apiEndpoint = "/api/referrals/partner-stats") {
1877
2517
  };
1878
2518
  }
1879
2519
  }
1880
-
1881
2520
  //#endregion
1882
2521
  //#region src/utilities/getCartTotalWithDiscounts.ts
1883
2522
  /**
@@ -1893,66 +2532,6 @@ function getCartTotalWithDiscounts(cart) {
1893
2532
  const customerDiscount = cart.customerDiscount ?? 0;
1894
2533
  return roundTo2(Math.max(0, subtotal - discountAmount - customerDiscount));
1895
2534
  }
1896
-
1897
- //#endregion
1898
- //#region src/utilities/recordCouponUsageForOrder.ts
1899
- /**
1900
- * Record coupon and referral usage when an order is placed successfully.
1901
- * Call this once when the order is created/paid (e.g. from Orders collection afterChange hook).
1902
- *
1903
- * - Coupon: increments the coupon's usageCount.
1904
- * - Referral: increments the referral code's usageCount and successfulReferralsCount,
1905
- * and adds order.partnerCommission to totalEarnings and pendingEarnings (partner gets commission;
1906
- * referee discount is already on the order).
1907
- */
1908
- async function recordCouponUsageForOrder(payload, order, pluginConfig) {
1909
- const result = {
1910
- recordedCoupon: false,
1911
- recordedReferral: false
1912
- };
1913
- const couponId = order.appliedCoupon == null ? null : typeof order.appliedCoupon === "string" ? order.appliedCoupon : order.appliedCoupon?.id;
1914
- const referralCodeId = order.appliedReferralCode == null ? null : typeof order.appliedReferralCode === "string" ? order.appliedReferralCode : order.appliedReferralCode?.id;
1915
- if (couponId) {
1916
- const coupon = await payload.findByID({
1917
- collection: pluginConfig.collections.couponsSlug,
1918
- id: couponId
1919
- });
1920
- if (coupon) {
1921
- await payload.update({
1922
- collection: pluginConfig.collections.couponsSlug,
1923
- id: couponId,
1924
- data: { usageCount: (coupon.usageCount ?? 0) + 1 }
1925
- });
1926
- result.recordedCoupon = true;
1927
- }
1928
- }
1929
- if (referralCodeId) {
1930
- const referralCode = await payload.findByID({
1931
- collection: pluginConfig.collections.referralCodesSlug,
1932
- id: referralCodeId
1933
- });
1934
- if (referralCode) {
1935
- const commission = Number(order.partnerCommission) || 0;
1936
- const currentTotal = Number(referralCode.totalEarnings) || 0;
1937
- const currentPending = Number(referralCode.pendingEarnings) || 0;
1938
- const currentUsageCount = Number(referralCode.usageCount) || 0;
1939
- const currentSuccessful = Number(referralCode.successfulReferralsCount) || 0;
1940
- await payload.update({
1941
- collection: pluginConfig.collections.referralCodesSlug,
1942
- id: referralCodeId,
1943
- data: {
1944
- usageCount: currentUsageCount + 1,
1945
- successfulReferralsCount: currentSuccessful + 1,
1946
- totalEarnings: currentTotal + commission,
1947
- pendingEarnings: currentPending + commission
1948
- }
1949
- });
1950
- result.recordedReferral = true;
1951
- }
1952
- }
1953
- return result;
1954
- }
1955
-
1956
2535
  //#endregion
1957
2536
  exports.calculateCommissionAndDiscount = calculateCommissionAndDiscount;
1958
2537
  exports.createCouponsCollection = createCouponsCollection;
@@ -1965,4 +2544,5 @@ exports.recordCouponUsageForOrder = recordCouponUsageForOrder;
1965
2544
  exports.useCouponCode = useCouponCode;
1966
2545
  exports.usePartnerStats = usePartnerStats;
1967
2546
  exports.validateCouponCode = validateCouponCode;
2547
+
1968
2548
  //# sourceMappingURL=index.js.map