mumz-strapi-plugin-coupon 1.0.7 → 1.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,311 +1,22 @@
1
- # Strapi Plugin - Coupon Management
2
-
1
+ #Strapi Plugin - Coupon Management
3
2
  A centralized coupon management system for Strapi that handles validation, redemption, and tracking across multiple service domains.
4
3
 
5
4
  ## Requirements
6
-
7
5
  - Strapi 5.0.0 or higher
8
6
  - Node.js 18.0.0 or higher
9
7
  - npm 6.0.0 or higher
10
8
  - PostgreSQL, MySQL, or SQLite
11
9
 
12
10
  ## Features
13
-
14
- - Centralized coupon creation and management
15
- - Cross-domain coupon support (Homecare, Nannies, Gear Refresh, etc.)
16
- - Flexible discount types (percentage, flat)
17
- - Usage limits and validity periods
18
- - User eligibility validation
19
- - Service-specific applicability
20
- - Complete redemption audit trail
21
- - RESTful API endpoints
22
- - Race condition protection for concurrent redemptions
23
- - Input validation and sanitization
24
- - Rate limiting on public endpoints
25
- - ✅ TypeScript support with full type definitions
26
-
27
- ## Installation
28
-
29
- ### Using npm (via GitHub)
30
-
31
- ```bash
32
- npm install github:mumzworld-tech/mumz-services-coupons-strapi-plugin
33
- ```
34
-
35
- ### Using yarn (via GitHub)
36
-
37
- ```bash
38
- yarn add github:mumzworld-tech/mumz-services-coupons-strapi-plugin
39
- ```
40
-
41
- ## Configuration
42
-
43
- Add the plugin to your Strapi configuration:
44
-
45
- **`config/plugins.js`** or **`config/plugins.ts`**:
46
-
47
- ```javascript
48
- module.exports = {
49
- 'coupon': {
50
- enabled: true,
51
- resolve: './node_modules/mumz-strapi-plugin-coupon'
52
- },
53
- };
54
- ```
55
-
56
- ## API Endpoints
57
-
58
- ### 1. Validate Coupon
59
-
60
- **POST** `/api/coupon/validate`
61
-
62
- Validates a coupon for a user before applying it.
63
-
64
- **Request Body:**
65
- ```json
66
- {
67
- "couponCode": "WELCOME10",
68
- "phoneNumber": "+97155XXXXXX",
69
- "orderAmount": 250.00
70
- }
71
- ```
72
-
73
- **Success Response:**
74
- ```json
75
- {
76
- "isValid": true,
77
- "discountType": "percentage",
78
- "discountValue": 10,
79
- "discountAmount": 25,
80
- "finalAmount": 225.00,
81
- "message": "Coupon applied successfully."
82
- }
83
- ```
84
-
85
- **Error Response:**
86
- ```json
87
- {
88
- "isValid": false,
89
- "errorCode": "COUPON_EXPIRED",
90
- "message": "This coupon has expired."
91
- }
92
- ```
93
-
94
- ### 2. Redeem Coupon
95
-
96
- **POST** `/api/coupon/redeem`
97
-
98
- Marks a coupon as redeemed once successfully applied.
99
-
100
- **Request Body:**
101
- ```json
102
- {
103
- "couponCode": "WELCOME10",
104
- "phoneNumber": "+97155XXXXXX",
105
- "orderId": "order_67890",
106
- "orderAmount": 250.00
107
- }
108
- ```
109
-
110
- **Response:**
111
- ```json
112
- {
113
- "success": true,
114
- "message": "Coupon redeemed successfully.",
115
- "redemptionId": "redeem_1"
116
- }
117
- ```
118
-
119
- ### 3. Create Coupon (No Auth Required)
120
-
121
- **POST** `/api/coupon`
122
-
123
- Create new coupons. Authentication has been disabled for easier integration.
124
-
125
- **Request Body:**
126
- ```json
127
- {
128
- "code": "SUMMER25",
129
- "discountType": "flat",
130
- "discountValue": 25,
131
- "maxUsage": 1000,
132
- "validFrom": "2025-05-01T00:00:00Z",
133
- "validTo": "2025-06-30T23:59:59Z"
134
- }
135
- ```
136
-
137
- **Response:**
138
- ```json
139
- {
140
- "couponId": "coupon_abc123",
141
- "message": "Coupon created successfully."
142
- }
143
- ```
144
-
145
- ## Error Codes
146
-
147
- | Code | Description |
148
- |------|-------------|
149
- | `COUPON_NOT_FOUND` | Coupon code does not exist |
150
- | `COUPON_EXPIRED` | Coupon validity period has expired |
151
- | `COUPON_ALREADY_USED` | User has already redeemed this coupon |
152
- | `USAGE_LIMIT_REACHED` | Coupon usage limit has been reached |
153
-
154
- ## Quick Start
155
-
156
- ### 1. Installation
157
-
158
- See [INSTALLATION.md](./INSTALLATION.md) for detailed installation instructions.
159
-
160
- ```bash
161
- # Install from GitHub
162
- npm install github:mumzworld-tech/mumz-services-coupons-strapi-plugin#v1.0.0
163
-
164
- # Configure in config/plugins.js
165
- module.exports = {
166
- 'coupon': {
167
- enabled: true,
168
- resolve: './node_modules/mumz-strapi-plugin-coupon',
169
- },
170
- };
171
-
172
- # Build and start Strapi
173
- npm run build
174
- npm run develop
175
- ```
176
-
177
- ### 2. Verify Installation
178
-
179
- Check that the plugin loaded successfully:
180
- - Admin panel shows **Coupon** and **Redemption** content types
181
- - Routes are registered: `npx strapi routes:list | grep coupon`
182
- - See console logs: "Coupon plugin registered" and "Coupon plugin bootstrapped"
183
-
184
- ### 3. Test the Plugin
185
-
186
- See [TESTING.md](./TESTING.md) for comprehensive testing instructions.
187
-
188
- ```bash
189
- # Create a test coupon via API
190
- curl -X POST http://localhost:1337/api/coupon \
191
- -H "Content-Type: application/json" \
192
- -d '{
193
- "code": "WELCOME10",
194
- "discountType": "percentage",
195
- "discountValue": 10,
196
- "maxUsage": 1000,
197
- "validFrom": "2025-01-01T00:00:00Z",
198
- "validTo": "2025-12-31T23:59:59Z",
199
- "description": "Welcome discount - 10% off"
200
- }'
201
-
202
- # Validate the coupon
203
- curl -X POST http://localhost:1337/api/coupon/validate \
204
- -H "Content-Type: application/json" \
205
- -d '{
206
- "couponCode": "WELCOME10",
207
- "phoneNumber": "+971551234567",
208
- "orderAmount": 250.00
209
- }'
210
- ```
211
-
212
- ## Development
213
-
214
- ### Prerequisites
215
-
216
- - Node.js >= 18.x
217
- - npm >= 6.x
218
-
219
- ### Setup
220
-
221
- ```bash
222
- # Install dependencies
223
- npm install
224
-
225
- # Build the plugin
226
- npm run build
227
-
228
- # Watch mode for development
229
- npm run watch
230
- ```
231
-
232
- ### Building for Production
233
-
234
- ```bash
235
- npm run clean
236
- npm run build
237
- ```
238
-
239
- ## Publishing to GitHub
240
-
241
- See [PUBLISHING.md](./PUBLISHING.md) for detailed instructions on publishing this plugin internally via GitHub.
242
-
243
- ## TypeScript Usage
244
-
245
- The plugin is written in TypeScript and provides full type definitions:
246
-
247
- ```typescript
248
- import type {
249
- ValidateCouponRequest,
250
- ValidationResponse,
251
- RedeemCouponRequest,
252
- RedemptionResponse,
253
- CreateCouponRequest,
254
- CreateResponse
255
- } from 'strapi-plugin-coupon/dist/services/coupon';
256
-
257
- // Example: Validate a coupon
258
- const result: ValidationResponse = await strapi
259
- .plugin('coupon')
260
- .service('coupon')
261
- .validate({
262
- couponCode: 'WELCOME10',
263
- service: 'homecare',
264
- phoneNumber: '+971551234567',
265
- orderAmount: 250.00,
266
- });
267
-
268
- if (result.isValid) {
269
- console.log(`Discount: ${result.discountAmount}`);
270
- console.log(`Final Amount: ${result.finalAmount}`);
271
- }
272
- ```
273
-
274
- ## Rate Limiting
275
-
276
- The plugin implements rate limiting on public endpoints:
277
- - `/validate`: 10 requests per minute per IP
278
- - `/redeem`: 5 requests per minute per IP
279
-
280
- The plugin uses in-memory rate limiting which is suitable for single-instance deployments.
281
-
282
- ## Security Features
283
-
284
- - ✅ Input validation on all endpoints
285
- - ✅ Protection against SQL injection
286
- - ✅ Race condition prevention using optimistic locking
287
- - ✅ Phone number validation (E.164 format)
288
- - ✅ Service and coupon code validation
289
- - ✅ Rate limiting to prevent abuse
290
-
291
- ## Troubleshooting
292
-
293
- ### Build fails with TypeScript errors
294
-
295
- Ensure dependencies are installed:
296
- ```bash
297
- npm install
298
- npm run build
299
- ```
300
-
301
- ### Plugin not loading in Strapi
302
-
303
- Verify the plugin configuration in `config/plugins.js` and ensure the plugin is properly installed in `node_modules`.
304
-
305
- ### Rate limit errors (429)
306
-
307
- Default rate limits are 10 req/min for validate, 5 req/min for redeem. These can be adjusted in the route configuration.
308
-
309
- ## License
310
-
311
- MIT
11
+ ✅ Centralized coupon creation and management
12
+ Cross-domain coupon support (Homecare, Nannies, Gear Refresh, etc.)
13
+ Flexible discount types (percentage, flat)
14
+ Usage limits and validity periods
15
+ User eligibility validation
16
+ Service-specific applicability
17
+ Complete redemption audit trail
18
+ RESTful API endpoints
19
+ Race condition protection for concurrent redemptions
20
+ Input validation and sanitization
21
+ Rate limiting on public endpoints
22
+ TypeScript support with full type definitions
@@ -50,6 +50,12 @@ declare const _default: {
50
50
  min: number;
51
51
  default: null;
52
52
  };
53
+ maxUsagePerUser: {
54
+ type: string;
55
+ required: boolean;
56
+ min: number;
57
+ default: null;
58
+ };
53
59
  currentUsage: {
54
60
  type: string;
55
61
  required: boolean;
@@ -49,6 +49,12 @@ declare const _default: {
49
49
  min: number;
50
50
  default: null;
51
51
  };
52
+ maxUsagePerUser: {
53
+ type: string;
54
+ required: boolean;
55
+ min: number;
56
+ default: null;
57
+ };
52
58
  currentUsage: {
53
59
  type: string;
54
60
  required: boolean;
@@ -21,8 +21,9 @@ exports.default = {
21
21
  },
22
22
  },
23
23
  layouts: {
24
- list: ['code', 'discountType', 'discountValue', 'currentUsage', 'maxUsage', 'isActive', 'validFrom', 'validTo'],
24
+ list: ['code', 'discountType', 'discountValue', 'currentUsage', 'maxUsage', 'maxUsagePerUser', 'isActive', 'validFrom', 'validTo'],
25
25
  edit: [
26
+ // -- Coupon Info --
26
27
  [
27
28
  { name: 'code', size: 6 },
28
29
  { name: 'isActive', size: 6 },
@@ -31,15 +32,20 @@ exports.default = {
31
32
  { name: 'discountType', size: 6 },
32
33
  { name: 'discountValue', size: 6 },
33
34
  ],
35
+ // -- Usage Limits --
34
36
  [
35
- { name: 'maxUsage', size: 6 },
36
- { name: 'currentUsage', size: 6 },
37
+ { name: 'maxUsage', size: 4 },
38
+ { name: 'maxUsagePerUser', size: 4 },
39
+ { name: 'currentUsage', size: 4 },
37
40
  ],
41
+ // -- Validity Period --
38
42
  [
39
43
  { name: 'validFrom', size: 6 },
40
44
  { name: 'validTo', size: 6 },
41
45
  ],
46
+ // -- Additional Info --
42
47
  [{ name: 'description', size: 12 }],
48
+ // -- Restrictions --
43
49
  [{ name: 'userRestrictions', size: 12 }],
44
50
  ],
45
51
  },
@@ -67,6 +73,12 @@ exports.default = {
67
73
  min: 0,
68
74
  default: null,
69
75
  },
76
+ maxUsagePerUser: {
77
+ type: 'integer',
78
+ required: false,
79
+ min: 0,
80
+ default: null,
81
+ },
70
82
  currentUsage: {
71
83
  type: 'integer',
72
84
  required: true,
@@ -51,6 +51,12 @@ declare const _default: {
51
51
  min: number;
52
52
  default: null;
53
53
  };
54
+ maxUsagePerUser: {
55
+ type: string;
56
+ required: boolean;
57
+ min: number;
58
+ default: null;
59
+ };
54
60
  currentUsage: {
55
61
  type: string;
56
62
  required: boolean;
package/dist/index.d.ts CHANGED
@@ -96,6 +96,12 @@ declare const plugin: {
96
96
  min: number;
97
97
  default: null;
98
98
  };
99
+ maxUsagePerUser: {
100
+ type: string;
101
+ required: boolean;
102
+ min: number;
103
+ default: null;
104
+ };
99
105
  currentUsage: {
100
106
  type: string;
101
107
  required: boolean;
@@ -1,10 +1,11 @@
1
- import type { Core } from '@strapi/strapi';
1
+ import type { Core } from "@strapi/strapi";
2
2
  export declare const ERROR_CODES: {
3
3
  readonly COUPON_NOT_FOUND: "COUPON_NOT_FOUND";
4
4
  readonly COUPON_EXPIRED: "COUPON_EXPIRED";
5
5
  readonly COUPON_ALREADY_USED: "COUPON_ALREADY_USED";
6
6
  readonly USAGE_LIMIT_REACHED: "USAGE_LIMIT_REACHED";
7
7
  readonly INVALID_REQUEST: "INVALID_REQUEST";
8
+ readonly USER_RESTRICTED: "USER_RESTRICTED";
8
9
  };
9
10
  export declare const ERROR_MESSAGES: {
10
11
  readonly COUPON_NOT_FOUND: "Coupon code does not exist.";
@@ -12,6 +13,7 @@ export declare const ERROR_MESSAGES: {
12
13
  readonly COUPON_ALREADY_USED: "This coupon has been already used.";
13
14
  readonly USAGE_LIMIT_REACHED: "Coupon usage limit has been reached.";
14
15
  readonly INVALID_REQUEST: "Invalid request parameters.";
16
+ readonly USER_RESTRICTED: "This coupon is not available for your phone number.";
15
17
  };
16
18
  export interface ValidateCouponRequest {
17
19
  couponCode: string;
@@ -26,13 +28,14 @@ export interface RedeemCouponRequest {
26
28
  }
27
29
  export interface CreateCouponRequest {
28
30
  code: string;
29
- discountType: 'percentage' | 'flat';
31
+ discountType: "percentage" | "flat";
30
32
  discountValue: number;
31
33
  maxUsage?: number;
34
+ maxUsagePerUser?: number;
32
35
  validFrom: string;
33
36
  validTo: string;
34
37
  description?: string;
35
- userRestrictions?: any;
38
+ userRestrictions?: string[];
36
39
  }
37
40
  export interface ValidationResponse {
38
41
  isValid: boolean;
@@ -4,18 +4,20 @@ exports.ERROR_MESSAGES = exports.ERROR_CODES = void 0;
4
4
  const validators_1 = require("../utils/validators");
5
5
  // Error codes as per specification
6
6
  exports.ERROR_CODES = {
7
- COUPON_NOT_FOUND: 'COUPON_NOT_FOUND',
8
- COUPON_EXPIRED: 'COUPON_EXPIRED',
9
- COUPON_ALREADY_USED: 'COUPON_ALREADY_USED',
10
- USAGE_LIMIT_REACHED: 'USAGE_LIMIT_REACHED',
11
- INVALID_REQUEST: 'INVALID_REQUEST',
7
+ COUPON_NOT_FOUND: "COUPON_NOT_FOUND",
8
+ COUPON_EXPIRED: "COUPON_EXPIRED",
9
+ COUPON_ALREADY_USED: "COUPON_ALREADY_USED",
10
+ USAGE_LIMIT_REACHED: "USAGE_LIMIT_REACHED",
11
+ INVALID_REQUEST: "INVALID_REQUEST",
12
+ USER_RESTRICTED: "USER_RESTRICTED",
12
13
  };
13
14
  exports.ERROR_MESSAGES = {
14
- COUPON_NOT_FOUND: 'Coupon code does not exist.',
15
- COUPON_EXPIRED: 'This coupon has expired.',
16
- COUPON_ALREADY_USED: 'This coupon has been already used.',
17
- USAGE_LIMIT_REACHED: 'Coupon usage limit has been reached.',
18
- INVALID_REQUEST: 'Invalid request parameters.',
15
+ COUPON_NOT_FOUND: "Coupon code does not exist.",
16
+ COUPON_EXPIRED: "This coupon has expired.",
17
+ COUPON_ALREADY_USED: "This coupon has been already used.",
18
+ USAGE_LIMIT_REACHED: "Coupon usage limit has been reached.",
19
+ INVALID_REQUEST: "Invalid request parameters.",
20
+ USER_RESTRICTED: "This coupon is not available for your phone number.",
19
21
  };
20
22
  const couponService = ({ strapi }) => ({
21
23
  /**
@@ -29,25 +31,25 @@ const couponService = ({ strapi }) => ({
29
31
  return {
30
32
  isValid: false,
31
33
  errorCode: exports.ERROR_CODES.INVALID_REQUEST,
32
- message: 'Invalid coupon code format',
34
+ message: "Invalid coupon code format",
33
35
  };
34
36
  }
35
37
  if (!(0, validators_1.validatePhoneNumber)(phoneNumber)) {
36
38
  return {
37
39
  isValid: false,
38
40
  errorCode: exports.ERROR_CODES.INVALID_REQUEST,
39
- message: 'Invalid phone number format',
41
+ message: "Invalid phone number format",
40
42
  };
41
43
  }
42
44
  if (orderAmount < 0) {
43
45
  return {
44
46
  isValid: false,
45
47
  errorCode: exports.ERROR_CODES.INVALID_REQUEST,
46
- message: 'Order amount cannot be negative',
48
+ message: "Order amount cannot be negative",
47
49
  };
48
50
  }
49
51
  // Find the coupon by code
50
- const coupons = await strapi.entityService.findMany('plugin::coupon.coupon', {
52
+ const coupons = await strapi.entityService.findMany("plugin::coupon.coupon", {
51
53
  filters: {
52
54
  code: couponCode,
53
55
  },
@@ -88,14 +90,19 @@ const couponService = ({ strapi }) => ({
88
90
  message: exports.ERROR_MESSAGES.USAGE_LIMIT_REACHED,
89
91
  };
90
92
  }
91
- // Check if user has already used this coupon
92
- const existingRedemptions = await strapi.entityService.findMany('plugin::coupon.redemption', {
93
+ // Check per-user usage limit
94
+ const existingRedemptions = await strapi.entityService.findMany("plugin::coupon.redemption", {
93
95
  filters: {
94
96
  coupon: coupon.id,
95
97
  phoneNumber,
96
98
  },
97
99
  });
98
- if (Array.isArray(existingRedemptions) && existingRedemptions.length > 0) {
100
+ const userRedemptionCount = Array.isArray(existingRedemptions)
101
+ ? existingRedemptions.length
102
+ : 0;
103
+ // maxUsagePerUser: 0 = disabled, null/undefined = 1, N = N uses per user
104
+ const perUserLimit = coupon.maxUsagePerUser ?? 1;
105
+ if (perUserLimit === 0 || userRedemptionCount >= perUserLimit) {
99
106
  return {
100
107
  isValid: false,
101
108
  errorCode: exports.ERROR_CODES.COUPON_ALREADY_USED,
@@ -105,11 +112,11 @@ const couponService = ({ strapi }) => ({
105
112
  // Calculate discount
106
113
  let discountAmount = 0;
107
114
  let finalAmount = orderAmount;
108
- if (coupon.discountType === 'percentage') {
115
+ if (coupon.discountType === "percentage") {
109
116
  discountAmount = (orderAmount * coupon.discountValue) / 100;
110
117
  finalAmount = orderAmount - discountAmount;
111
118
  }
112
- else if (coupon.discountType === 'flat') {
119
+ else if (coupon.discountType === "flat") {
113
120
  discountAmount = coupon.discountValue;
114
121
  finalAmount = Math.max(0, orderAmount - discountAmount);
115
122
  }
@@ -120,11 +127,11 @@ const couponService = ({ strapi }) => ({
120
127
  discountValue: coupon.discountValue,
121
128
  discountAmount,
122
129
  finalAmount,
123
- message: 'Coupon applied successfully.',
130
+ message: "Coupon applied successfully.",
124
131
  };
125
132
  }
126
133
  catch (error) {
127
- strapi.log.error('Error validating coupon:', error);
134
+ strapi.log.error("Error validating coupon:", error);
128
135
  throw error;
129
136
  }
130
137
  },
@@ -140,11 +147,11 @@ const couponService = ({ strapi }) => ({
140
147
  return {
141
148
  success: false,
142
149
  errorCode: exports.ERROR_CODES.INVALID_REQUEST,
143
- message: 'Invalid order ID format',
150
+ message: "Invalid order ID format",
144
151
  };
145
152
  }
146
153
  // Find and validate the coupon
147
- const coupons = await strapi.db.query('plugin::coupon.coupon').findMany({
154
+ const coupons = await strapi.db.query("plugin::coupon.coupon").findMany({
148
155
  where: { code: couponCode },
149
156
  });
150
157
  const coupon = Array.isArray(coupons) && coupons.length > 0 ? coupons[0] : null;
@@ -160,7 +167,7 @@ const couponService = ({ strapi }) => ({
160
167
  return {
161
168
  success: false,
162
169
  errorCode: exports.ERROR_CODES.INVALID_REQUEST,
163
- message: 'Invalid phone number format',
170
+ message: "Invalid phone number format",
164
171
  };
165
172
  }
166
173
  // Perform all validations
@@ -181,14 +188,31 @@ const couponService = ({ strapi }) => ({
181
188
  message: exports.ERROR_MESSAGES.COUPON_EXPIRED,
182
189
  };
183
190
  }
184
- // Check for existing redemption
185
- const existingRedemptions = await strapi.db.query('plugin::coupon.redemption').findMany({
191
+ // Check user restrictions (blocked phone numbers)
192
+ if (coupon.userRestrictions && Array.isArray(coupon.userRestrictions)) {
193
+ if (coupon.userRestrictions.includes(phoneNumber)) {
194
+ return {
195
+ success: false,
196
+ errorCode: exports.ERROR_CODES.USER_RESTRICTED,
197
+ message: exports.ERROR_MESSAGES.USER_RESTRICTED,
198
+ };
199
+ }
200
+ }
201
+ // Check per-user usage limit
202
+ const existingRedemptions = await strapi.db
203
+ .query("plugin::coupon.redemption")
204
+ .findMany({
186
205
  where: {
187
206
  coupon: coupon.id,
188
207
  phoneNumber,
189
208
  },
190
209
  });
191
- if (Array.isArray(existingRedemptions) && existingRedemptions.length > 0) {
210
+ const userRedemptionCount = Array.isArray(existingRedemptions)
211
+ ? existingRedemptions.length
212
+ : 0;
213
+ // maxUsagePerUser: 0 = disabled, null/undefined = 1, N = N uses per user
214
+ const perUserLimit = coupon.maxUsagePerUser ?? 1;
215
+ if (perUserLimit === 0 || userRedemptionCount >= perUserLimit) {
192
216
  return {
193
217
  success: false,
194
218
  errorCode: exports.ERROR_CODES.COUPON_ALREADY_USED,
@@ -206,11 +230,11 @@ const couponService = ({ strapi }) => ({
206
230
  // Calculate discount
207
231
  let discountAmount = 0;
208
232
  let finalAmount = orderAmount;
209
- if (coupon.discountType === 'percentage') {
233
+ if (coupon.discountType === "percentage") {
210
234
  discountAmount = (orderAmount * coupon.discountValue) / 100;
211
235
  finalAmount = orderAmount - discountAmount;
212
236
  }
213
- else if (coupon.discountType === 'flat') {
237
+ else if (coupon.discountType === "flat") {
214
238
  discountAmount = coupon.discountValue;
215
239
  finalAmount = Math.max(0, orderAmount - discountAmount);
216
240
  }
@@ -218,7 +242,9 @@ const couponService = ({ strapi }) => ({
218
242
  let redemptionId;
219
243
  try {
220
244
  // Atomic increment with optimistic locking
221
- const updateResult = await strapi.db.query('plugin::coupon.coupon').update({
245
+ const updateResult = await strapi.db
246
+ .query("plugin::coupon.coupon")
247
+ .update({
222
248
  where: {
223
249
  id: coupon.id,
224
250
  currentUsage: coupon.currentUsage, // Optimistic lock
@@ -228,7 +254,8 @@ const couponService = ({ strapi }) => ({
228
254
  },
229
255
  });
230
256
  // If update failed due to concurrent modification, check usage limit
231
- if (!updateResult || (coupon.maxUsage && coupon.currentUsage + 1 > coupon.maxUsage)) {
257
+ if (!updateResult ||
258
+ (coupon.maxUsage && coupon.currentUsage + 1 > coupon.maxUsage)) {
232
259
  return {
233
260
  success: false,
234
261
  errorCode: exports.ERROR_CODES.USAGE_LIMIT_REACHED,
@@ -236,7 +263,9 @@ const couponService = ({ strapi }) => ({
236
263
  };
237
264
  }
238
265
  // Create redemption record
239
- const redemption = await strapi.db.query('plugin::coupon.redemption').create({
266
+ const redemption = await strapi.db
267
+ .query("plugin::coupon.redemption")
268
+ .create({
240
269
  data: {
241
270
  coupon: coupon.id,
242
271
  phoneNumber,
@@ -254,17 +283,17 @@ const couponService = ({ strapi }) => ({
254
283
  redemptionId = `redeem_${redemption.id}`;
255
284
  }
256
285
  catch (error) {
257
- strapi.log.error('Transaction error during redemption:', error);
286
+ strapi.log.error("Transaction error during redemption:", error);
258
287
  throw error;
259
288
  }
260
289
  return {
261
290
  success: true,
262
- message: 'Coupon redeemed successfully.',
291
+ message: "Coupon redeemed successfully.",
263
292
  redemptionId,
264
293
  };
265
294
  }
266
295
  catch (error) {
267
- strapi.log.error('Error redeeming coupon:', error);
296
+ strapi.log.error("Error redeeming coupon:", error);
268
297
  throw error;
269
298
  }
270
299
  },
@@ -273,13 +302,13 @@ const couponService = ({ strapi }) => ({
273
302
  */
274
303
  async create(params) {
275
304
  try {
276
- const { code, discountType, discountValue, maxUsage, validFrom, validTo, description, userRestrictions, } = params;
305
+ const { code, discountType, discountValue, maxUsage, maxUsagePerUser, validFrom, validTo, description, userRestrictions, } = params;
277
306
  // Validate coupon code
278
307
  if (!(0, validators_1.validateCouponCode)(code)) {
279
308
  return {
280
309
  success: false,
281
- errorCode: 'INVALID_COUPON_CODE',
282
- message: 'Coupon code must be 3-50 uppercase alphanumeric characters',
310
+ errorCode: "INVALID_COUPON_CODE",
311
+ message: "Coupon code must be 3-50 uppercase alphanumeric characters",
283
312
  };
284
313
  }
285
314
  // Validate discount value
@@ -287,8 +316,8 @@ const couponService = ({ strapi }) => ({
287
316
  if (!discountValidation.valid) {
288
317
  return {
289
318
  success: false,
290
- errorCode: 'INVALID_DISCOUNT',
291
- message: discountValidation.error || 'Invalid discount value',
319
+ errorCode: "INVALID_DISCOUNT",
320
+ message: discountValidation.error || "Invalid discount value",
292
321
  };
293
322
  }
294
323
  // Validate date range
@@ -296,20 +325,21 @@ const couponService = ({ strapi }) => ({
296
325
  if (!dateValidation.valid) {
297
326
  return {
298
327
  success: false,
299
- errorCode: 'INVALID_DATE_RANGE',
300
- message: dateValidation.error || 'Invalid date range',
328
+ errorCode: "INVALID_DATE_RANGE",
329
+ message: dateValidation.error || "Invalid date range",
301
330
  };
302
331
  }
303
332
  // Validate maxUsage
304
- if (maxUsage !== undefined && (typeof maxUsage !== 'number' || maxUsage < 1)) {
333
+ if (maxUsage !== undefined &&
334
+ (typeof maxUsage !== "number" || maxUsage < 1)) {
305
335
  return {
306
336
  success: false,
307
- errorCode: 'INVALID_MAX_USAGE',
308
- message: 'maxUsage must be a positive number',
337
+ errorCode: "INVALID_MAX_USAGE",
338
+ message: "maxUsage must be a positive number",
309
339
  };
310
340
  }
311
341
  // Check if coupon code already exists
312
- const existingCoupons = await strapi.entityService.findMany('plugin::coupon.coupon', {
342
+ const existingCoupons = await strapi.entityService.findMany("plugin::coupon.coupon", {
313
343
  filters: {
314
344
  code,
315
345
  },
@@ -317,17 +347,18 @@ const couponService = ({ strapi }) => ({
317
347
  if (Array.isArray(existingCoupons) && existingCoupons.length > 0) {
318
348
  return {
319
349
  success: false,
320
- errorCode: 'COUPON_ALREADY_EXISTS',
321
- message: 'A coupon with this code already exists.',
350
+ errorCode: "COUPON_ALREADY_EXISTS",
351
+ message: "A coupon with this code already exists.",
322
352
  };
323
353
  }
324
354
  // Create the coupon
325
- const coupon = await strapi.entityService.create('plugin::coupon.coupon', {
355
+ const coupon = await strapi.entityService.create("plugin::coupon.coupon", {
326
356
  data: {
327
357
  code,
328
358
  discountType,
329
359
  discountValue,
330
360
  maxUsage: maxUsage || null,
361
+ maxUsagePerUser: maxUsagePerUser ?? null,
331
362
  currentUsage: 0,
332
363
  validFrom,
333
364
  validTo,
@@ -339,11 +370,11 @@ const couponService = ({ strapi }) => ({
339
370
  return {
340
371
  success: true,
341
372
  couponId: `coupon_${coupon.id}`,
342
- message: 'Coupon created successfully.',
373
+ message: "Coupon created successfully.",
343
374
  };
344
375
  }
345
376
  catch (error) {
346
- strapi.log.error('Error creating coupon:', error);
377
+ strapi.log.error("Error creating coupon:", error);
347
378
  throw error;
348
379
  }
349
380
  },
@@ -352,10 +383,10 @@ const couponService = ({ strapi }) => ({
352
383
  */
353
384
  async findAll(query = {}) {
354
385
  try {
355
- return await strapi.entityService.findMany('plugin::coupon.coupon', query);
386
+ return await strapi.entityService.findMany("plugin::coupon.coupon", query);
356
387
  }
357
388
  catch (error) {
358
- strapi.log.error('Error finding coupons:', error);
389
+ strapi.log.error("Error finding coupons:", error);
359
390
  throw error;
360
391
  }
361
392
  },
@@ -364,10 +395,10 @@ const couponService = ({ strapi }) => ({
364
395
  */
365
396
  async findOne(id) {
366
397
  try {
367
- return await strapi.entityService.findOne('plugin::coupon.coupon', id);
398
+ return await strapi.entityService.findOne("plugin::coupon.coupon", id);
368
399
  }
369
400
  catch (error) {
370
- strapi.log.error('Error finding coupon:', error);
401
+ strapi.log.error("Error finding coupon:", error);
371
402
  throw error;
372
403
  }
373
404
  },
@@ -376,10 +407,12 @@ const couponService = ({ strapi }) => ({
376
407
  */
377
408
  async update(id, data) {
378
409
  try {
379
- return await strapi.entityService.update('plugin::coupon.coupon', id, { data });
410
+ return await strapi.entityService.update("plugin::coupon.coupon", id, {
411
+ data,
412
+ });
380
413
  }
381
414
  catch (error) {
382
- strapi.log.error('Error updating coupon:', error);
415
+ strapi.log.error("Error updating coupon:", error);
383
416
  throw error;
384
417
  }
385
418
  },
@@ -388,10 +421,10 @@ const couponService = ({ strapi }) => ({
388
421
  */
389
422
  async delete(id) {
390
423
  try {
391
- return await strapi.entityService.delete('plugin::coupon.coupon', id);
424
+ return await strapi.entityService.delete("plugin::coupon.coupon", id);
392
425
  }
393
426
  catch (error) {
394
- strapi.log.error('Error deleting coupon:', error);
427
+ strapi.log.error("Error deleting coupon:", error);
395
428
  throw error;
396
429
  }
397
430
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mumz-strapi-plugin-coupon",
3
- "version": "1.0.7",
3
+ "version": "1.1.1",
4
4
  "description": "Strapi plugin for centralized coupon management across multiple services",
5
5
  "strapi": {
6
6
  "name": "coupon",