mumz-strapi-plugin-coupon 1.1.1 → 3.0.0
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 +15 -13
- package/dist/admin/index.js +677 -0
- package/dist/admin/index.mjs +678 -0
- package/dist/server/index.js +1201 -0
- package/dist/server/index.mjs +1202 -0
- package/package.json +44 -20
- package/strapi-admin.js +3 -0
- package/strapi-server.js +3 -1
- package/dist/bootstrap.d.ts +0 -5
- package/dist/bootstrap.js +0 -6
- package/dist/config/index.d.ts +0 -5
- package/dist/config/index.js +0 -6
- package/dist/content-types/coupon/index.d.ts +0 -96
- package/dist/content-types/coupon/index.js +0 -9
- package/dist/content-types/coupon/schema.d.ts +0 -94
- package/dist/content-types/coupon/schema.js +0 -117
- package/dist/content-types/index.d.ts +0 -161
- package/dist/content-types/index.js +0 -11
- package/dist/content-types/redemption/index.d.ts +0 -64
- package/dist/content-types/redemption/index.js +0 -9
- package/dist/content-types/redemption/schema.d.ts +0 -62
- package/dist/content-types/redemption/schema.js +0 -74
- package/dist/controllers/coupon.d.ts +0 -41
- package/dist/controllers/coupon.js +0 -154
- package/dist/controllers/index.d.ts +0 -5
- package/dist/controllers/index.js +0 -9
- package/dist/destroy.d.ts +0 -5
- package/dist/destroy.js +0 -6
- package/dist/index.d.ts +0 -207
- package/dist/index.js +0 -24
- package/dist/middlewares/index.d.ts +0 -4
- package/dist/middlewares/index.js +0 -9
- package/dist/middlewares/rate-limit.d.ts +0 -6
- package/dist/middlewares/rate-limit.js +0 -42
- package/dist/register.d.ts +0 -5
- package/dist/register.js +0 -6
- package/dist/routes/content-api/index.d.ts +0 -23
- package/dist/routes/content-api/index.js +0 -76
- package/dist/routes/index.d.ts +0 -25
- package/dist/routes/index.js +0 -9
- package/dist/services/coupon.d.ts +0 -64
- package/dist/services/coupon.js +0 -432
- package/dist/services/index.d.ts +0 -5
- package/dist/services/index.js +0 -9
- package/dist/utils/validators.d.ts +0 -14
- package/dist/utils/validators.js +0 -41
|
@@ -1,42 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
// Simple in-memory rate limiter
|
|
4
|
-
// Uses in-memory storage - suitable for single-instance deployments
|
|
5
|
-
const rateLimitStore = new Map();
|
|
6
|
-
// Clean up old entries every hour
|
|
7
|
-
setInterval(() => {
|
|
8
|
-
const now = Date.now();
|
|
9
|
-
for (const [key, value] of rateLimitStore.entries()) {
|
|
10
|
-
if (now > value.resetAt) {
|
|
11
|
-
rateLimitStore.delete(key);
|
|
12
|
-
}
|
|
13
|
-
}
|
|
14
|
-
}, 3600000); // 1 hour
|
|
15
|
-
exports.default = (config) => {
|
|
16
|
-
return async (ctx, next) => {
|
|
17
|
-
const identifier = ctx.ip || ctx.request.ip || 'unknown';
|
|
18
|
-
const now = Date.now();
|
|
19
|
-
const record = rateLimitStore.get(identifier);
|
|
20
|
-
// No record or expired, create new
|
|
21
|
-
if (!record || now > record.resetAt) {
|
|
22
|
-
rateLimitStore.set(identifier, {
|
|
23
|
-
count: 1,
|
|
24
|
-
resetAt: now + config.windowMs,
|
|
25
|
-
});
|
|
26
|
-
return next();
|
|
27
|
-
}
|
|
28
|
-
// Check if limit reached
|
|
29
|
-
if (record.count >= config.maxRequests) {
|
|
30
|
-
ctx.status = 429;
|
|
31
|
-
ctx.body = {
|
|
32
|
-
error: 'Too many requests',
|
|
33
|
-
message: `Rate limit exceeded. Please try again in ${Math.ceil((record.resetAt - now) / 1000)} seconds.`,
|
|
34
|
-
retryAfter: Math.ceil((record.resetAt - now) / 1000),
|
|
35
|
-
};
|
|
36
|
-
return;
|
|
37
|
-
}
|
|
38
|
-
// Increment counter
|
|
39
|
-
record.count++;
|
|
40
|
-
return next();
|
|
41
|
-
};
|
|
42
|
-
};
|
package/dist/register.d.ts
DELETED
package/dist/register.js
DELETED
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
declare const _default: {
|
|
2
|
-
type: string;
|
|
3
|
-
routes: ({
|
|
4
|
-
method: string;
|
|
5
|
-
path: string;
|
|
6
|
-
handler: string;
|
|
7
|
-
config: {
|
|
8
|
-
policies: never[];
|
|
9
|
-
middlewares: ((ctx: any, next: any) => Promise<any>)[];
|
|
10
|
-
auth: boolean;
|
|
11
|
-
};
|
|
12
|
-
} | {
|
|
13
|
-
method: string;
|
|
14
|
-
path: string;
|
|
15
|
-
handler: string;
|
|
16
|
-
config: {
|
|
17
|
-
policies: never[];
|
|
18
|
-
auth: boolean;
|
|
19
|
-
middlewares?: undefined;
|
|
20
|
-
};
|
|
21
|
-
})[];
|
|
22
|
-
};
|
|
23
|
-
export default _default;
|
|
@@ -1,76 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
-
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
-
};
|
|
5
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
const rate_limit_1 = __importDefault(require("../../middlewares/rate-limit"));
|
|
7
|
-
exports.default = {
|
|
8
|
-
type: 'content-api',
|
|
9
|
-
routes: [
|
|
10
|
-
{
|
|
11
|
-
method: 'POST',
|
|
12
|
-
path: '/validate',
|
|
13
|
-
handler: 'coupon.validate',
|
|
14
|
-
config: {
|
|
15
|
-
policies: [],
|
|
16
|
-
middlewares: [(0, rate_limit_1.default)({ maxRequests: 10, windowMs: 60000 })], // 10 requests per minute
|
|
17
|
-
auth: false,
|
|
18
|
-
},
|
|
19
|
-
},
|
|
20
|
-
{
|
|
21
|
-
method: 'POST',
|
|
22
|
-
path: '/redeem',
|
|
23
|
-
handler: 'coupon.redeem',
|
|
24
|
-
config: {
|
|
25
|
-
policies: [],
|
|
26
|
-
middlewares: [(0, rate_limit_1.default)({ maxRequests: 5, windowMs: 60000 })], // 5 requests per minute
|
|
27
|
-
auth: false,
|
|
28
|
-
},
|
|
29
|
-
},
|
|
30
|
-
{
|
|
31
|
-
method: 'POST',
|
|
32
|
-
path: '/',
|
|
33
|
-
handler: 'coupon.create',
|
|
34
|
-
config: {
|
|
35
|
-
policies: [],
|
|
36
|
-
auth: false,
|
|
37
|
-
},
|
|
38
|
-
},
|
|
39
|
-
{
|
|
40
|
-
method: 'GET',
|
|
41
|
-
path: '/',
|
|
42
|
-
handler: 'coupon.find',
|
|
43
|
-
config: {
|
|
44
|
-
policies: [],
|
|
45
|
-
auth: false,
|
|
46
|
-
},
|
|
47
|
-
},
|
|
48
|
-
{
|
|
49
|
-
method: 'GET',
|
|
50
|
-
path: '/:id',
|
|
51
|
-
handler: 'coupon.findOne',
|
|
52
|
-
config: {
|
|
53
|
-
policies: [],
|
|
54
|
-
auth: false,
|
|
55
|
-
},
|
|
56
|
-
},
|
|
57
|
-
{
|
|
58
|
-
method: 'PUT',
|
|
59
|
-
path: '/:id',
|
|
60
|
-
handler: 'coupon.update',
|
|
61
|
-
config: {
|
|
62
|
-
policies: [],
|
|
63
|
-
auth: false,
|
|
64
|
-
},
|
|
65
|
-
},
|
|
66
|
-
{
|
|
67
|
-
method: 'DELETE',
|
|
68
|
-
path: '/:id',
|
|
69
|
-
handler: 'coupon.delete',
|
|
70
|
-
config: {
|
|
71
|
-
policies: [],
|
|
72
|
-
auth: false,
|
|
73
|
-
},
|
|
74
|
-
},
|
|
75
|
-
],
|
|
76
|
-
};
|
package/dist/routes/index.d.ts
DELETED
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
declare const _default: {
|
|
2
|
-
'content-api': {
|
|
3
|
-
type: string;
|
|
4
|
-
routes: ({
|
|
5
|
-
method: string;
|
|
6
|
-
path: string;
|
|
7
|
-
handler: string;
|
|
8
|
-
config: {
|
|
9
|
-
policies: never[];
|
|
10
|
-
middlewares: ((ctx: any, next: any) => Promise<any>)[];
|
|
11
|
-
auth: boolean;
|
|
12
|
-
};
|
|
13
|
-
} | {
|
|
14
|
-
method: string;
|
|
15
|
-
path: string;
|
|
16
|
-
handler: string;
|
|
17
|
-
config: {
|
|
18
|
-
policies: never[];
|
|
19
|
-
auth: boolean;
|
|
20
|
-
middlewares?: undefined;
|
|
21
|
-
};
|
|
22
|
-
})[];
|
|
23
|
-
};
|
|
24
|
-
};
|
|
25
|
-
export default _default;
|
package/dist/routes/index.js
DELETED
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
-
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
-
};
|
|
5
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
const content_api_1 = __importDefault(require("./content-api"));
|
|
7
|
-
exports.default = {
|
|
8
|
-
'content-api': content_api_1.default,
|
|
9
|
-
};
|
|
@@ -1,64 +0,0 @@
|
|
|
1
|
-
import type { Core } from "@strapi/strapi";
|
|
2
|
-
export declare const ERROR_CODES: {
|
|
3
|
-
readonly COUPON_NOT_FOUND: "COUPON_NOT_FOUND";
|
|
4
|
-
readonly COUPON_EXPIRED: "COUPON_EXPIRED";
|
|
5
|
-
readonly COUPON_ALREADY_USED: "COUPON_ALREADY_USED";
|
|
6
|
-
readonly USAGE_LIMIT_REACHED: "USAGE_LIMIT_REACHED";
|
|
7
|
-
readonly INVALID_REQUEST: "INVALID_REQUEST";
|
|
8
|
-
readonly USER_RESTRICTED: "USER_RESTRICTED";
|
|
9
|
-
};
|
|
10
|
-
export declare const ERROR_MESSAGES: {
|
|
11
|
-
readonly COUPON_NOT_FOUND: "Coupon code does not exist.";
|
|
12
|
-
readonly COUPON_EXPIRED: "This coupon has expired.";
|
|
13
|
-
readonly COUPON_ALREADY_USED: "This coupon has been already used.";
|
|
14
|
-
readonly USAGE_LIMIT_REACHED: "Coupon usage limit has been reached.";
|
|
15
|
-
readonly INVALID_REQUEST: "Invalid request parameters.";
|
|
16
|
-
readonly USER_RESTRICTED: "This coupon is not available for your phone number.";
|
|
17
|
-
};
|
|
18
|
-
export interface ValidateCouponRequest {
|
|
19
|
-
couponCode: string;
|
|
20
|
-
phoneNumber: string;
|
|
21
|
-
orderAmount?: number;
|
|
22
|
-
}
|
|
23
|
-
export interface RedeemCouponRequest {
|
|
24
|
-
couponCode: string;
|
|
25
|
-
phoneNumber: string;
|
|
26
|
-
orderId: string;
|
|
27
|
-
orderAmount?: number;
|
|
28
|
-
}
|
|
29
|
-
export interface CreateCouponRequest {
|
|
30
|
-
code: string;
|
|
31
|
-
discountType: "percentage" | "flat";
|
|
32
|
-
discountValue: number;
|
|
33
|
-
maxUsage?: number;
|
|
34
|
-
maxUsagePerUser?: number;
|
|
35
|
-
validFrom: string;
|
|
36
|
-
validTo: string;
|
|
37
|
-
description?: string;
|
|
38
|
-
userRestrictions?: string[];
|
|
39
|
-
}
|
|
40
|
-
export interface ValidationResponse {
|
|
41
|
-
isValid: boolean;
|
|
42
|
-
errorCode?: string;
|
|
43
|
-
message: string;
|
|
44
|
-
discountType?: string;
|
|
45
|
-
discountValue?: number;
|
|
46
|
-
discountAmount?: number;
|
|
47
|
-
finalAmount?: number;
|
|
48
|
-
}
|
|
49
|
-
export interface RedemptionResponse {
|
|
50
|
-
success: boolean;
|
|
51
|
-
errorCode?: string;
|
|
52
|
-
message: string;
|
|
53
|
-
redemptionId?: string;
|
|
54
|
-
}
|
|
55
|
-
export interface CreateResponse {
|
|
56
|
-
success: boolean;
|
|
57
|
-
errorCode?: string;
|
|
58
|
-
message: string;
|
|
59
|
-
couponId?: string;
|
|
60
|
-
}
|
|
61
|
-
declare const couponService: ({ strapi }: {
|
|
62
|
-
strapi: Core.Strapi;
|
|
63
|
-
}) => any;
|
|
64
|
-
export default couponService;
|
package/dist/services/coupon.js
DELETED
|
@@ -1,432 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.ERROR_MESSAGES = exports.ERROR_CODES = void 0;
|
|
4
|
-
const validators_1 = require("../utils/validators");
|
|
5
|
-
// Error codes as per specification
|
|
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",
|
|
12
|
-
USER_RESTRICTED: "USER_RESTRICTED",
|
|
13
|
-
};
|
|
14
|
-
exports.ERROR_MESSAGES = {
|
|
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.",
|
|
21
|
-
};
|
|
22
|
-
const couponService = ({ strapi }) => ({
|
|
23
|
-
/**
|
|
24
|
-
* Validate a coupon for a specific service and user
|
|
25
|
-
*/
|
|
26
|
-
async validate(params) {
|
|
27
|
-
const { couponCode, phoneNumber, orderAmount = 0 } = params;
|
|
28
|
-
try {
|
|
29
|
-
// Validate inputs
|
|
30
|
-
if (!(0, validators_1.validateCouponCode)(couponCode)) {
|
|
31
|
-
return {
|
|
32
|
-
isValid: false,
|
|
33
|
-
errorCode: exports.ERROR_CODES.INVALID_REQUEST,
|
|
34
|
-
message: "Invalid coupon code format",
|
|
35
|
-
};
|
|
36
|
-
}
|
|
37
|
-
if (!(0, validators_1.validatePhoneNumber)(phoneNumber)) {
|
|
38
|
-
return {
|
|
39
|
-
isValid: false,
|
|
40
|
-
errorCode: exports.ERROR_CODES.INVALID_REQUEST,
|
|
41
|
-
message: "Invalid phone number format",
|
|
42
|
-
};
|
|
43
|
-
}
|
|
44
|
-
if (orderAmount < 0) {
|
|
45
|
-
return {
|
|
46
|
-
isValid: false,
|
|
47
|
-
errorCode: exports.ERROR_CODES.INVALID_REQUEST,
|
|
48
|
-
message: "Order amount cannot be negative",
|
|
49
|
-
};
|
|
50
|
-
}
|
|
51
|
-
// Find the coupon by code
|
|
52
|
-
const coupons = await strapi.entityService.findMany("plugin::coupon.coupon", {
|
|
53
|
-
filters: {
|
|
54
|
-
code: couponCode,
|
|
55
|
-
},
|
|
56
|
-
});
|
|
57
|
-
const coupon = Array.isArray(coupons) && coupons.length > 0 ? coupons[0] : null;
|
|
58
|
-
// Check if coupon exists
|
|
59
|
-
if (!coupon) {
|
|
60
|
-
return {
|
|
61
|
-
isValid: false,
|
|
62
|
-
errorCode: exports.ERROR_CODES.COUPON_NOT_FOUND,
|
|
63
|
-
message: exports.ERROR_MESSAGES.COUPON_NOT_FOUND,
|
|
64
|
-
};
|
|
65
|
-
}
|
|
66
|
-
// Check if coupon is active
|
|
67
|
-
if (!coupon.isActive) {
|
|
68
|
-
return {
|
|
69
|
-
isValid: false,
|
|
70
|
-
errorCode: exports.ERROR_CODES.COUPON_EXPIRED,
|
|
71
|
-
message: exports.ERROR_MESSAGES.COUPON_EXPIRED,
|
|
72
|
-
};
|
|
73
|
-
}
|
|
74
|
-
// Check validity period
|
|
75
|
-
const now = new Date();
|
|
76
|
-
const validFrom = new Date(coupon.validFrom);
|
|
77
|
-
const validTo = new Date(coupon.validTo);
|
|
78
|
-
if (now < validFrom || now > validTo) {
|
|
79
|
-
return {
|
|
80
|
-
isValid: false,
|
|
81
|
-
errorCode: exports.ERROR_CODES.COUPON_EXPIRED,
|
|
82
|
-
message: exports.ERROR_MESSAGES.COUPON_EXPIRED,
|
|
83
|
-
};
|
|
84
|
-
}
|
|
85
|
-
// Check usage limit
|
|
86
|
-
if (coupon.maxUsage && coupon.currentUsage >= coupon.maxUsage) {
|
|
87
|
-
return {
|
|
88
|
-
isValid: false,
|
|
89
|
-
errorCode: exports.ERROR_CODES.USAGE_LIMIT_REACHED,
|
|
90
|
-
message: exports.ERROR_MESSAGES.USAGE_LIMIT_REACHED,
|
|
91
|
-
};
|
|
92
|
-
}
|
|
93
|
-
// Check per-user usage limit
|
|
94
|
-
const existingRedemptions = await strapi.entityService.findMany("plugin::coupon.redemption", {
|
|
95
|
-
filters: {
|
|
96
|
-
coupon: coupon.id,
|
|
97
|
-
phoneNumber,
|
|
98
|
-
},
|
|
99
|
-
});
|
|
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) {
|
|
106
|
-
return {
|
|
107
|
-
isValid: false,
|
|
108
|
-
errorCode: exports.ERROR_CODES.COUPON_ALREADY_USED,
|
|
109
|
-
message: exports.ERROR_MESSAGES.COUPON_ALREADY_USED,
|
|
110
|
-
};
|
|
111
|
-
}
|
|
112
|
-
// Calculate discount
|
|
113
|
-
let discountAmount = 0;
|
|
114
|
-
let finalAmount = orderAmount;
|
|
115
|
-
if (coupon.discountType === "percentage") {
|
|
116
|
-
discountAmount = (orderAmount * coupon.discountValue) / 100;
|
|
117
|
-
finalAmount = orderAmount - discountAmount;
|
|
118
|
-
}
|
|
119
|
-
else if (coupon.discountType === "flat") {
|
|
120
|
-
discountAmount = coupon.discountValue;
|
|
121
|
-
finalAmount = Math.max(0, orderAmount - discountAmount);
|
|
122
|
-
}
|
|
123
|
-
// Return valid response
|
|
124
|
-
return {
|
|
125
|
-
isValid: true,
|
|
126
|
-
discountType: coupon.discountType,
|
|
127
|
-
discountValue: coupon.discountValue,
|
|
128
|
-
discountAmount,
|
|
129
|
-
finalAmount,
|
|
130
|
-
message: "Coupon applied successfully.",
|
|
131
|
-
};
|
|
132
|
-
}
|
|
133
|
-
catch (error) {
|
|
134
|
-
strapi.log.error("Error validating coupon:", error);
|
|
135
|
-
throw error;
|
|
136
|
-
}
|
|
137
|
-
},
|
|
138
|
-
/**
|
|
139
|
-
* Redeem a coupon after successful payment
|
|
140
|
-
* Uses database transaction to prevent race conditions
|
|
141
|
-
*/
|
|
142
|
-
async redeem(params) {
|
|
143
|
-
const { couponCode, phoneNumber, orderId, orderAmount = 0 } = params;
|
|
144
|
-
try {
|
|
145
|
-
// Validate inputs
|
|
146
|
-
if (!(0, validators_1.validateOrderId)(orderId)) {
|
|
147
|
-
return {
|
|
148
|
-
success: false,
|
|
149
|
-
errorCode: exports.ERROR_CODES.INVALID_REQUEST,
|
|
150
|
-
message: "Invalid order ID format",
|
|
151
|
-
};
|
|
152
|
-
}
|
|
153
|
-
// Find and validate the coupon
|
|
154
|
-
const coupons = await strapi.db.query("plugin::coupon.coupon").findMany({
|
|
155
|
-
where: { code: couponCode },
|
|
156
|
-
});
|
|
157
|
-
const coupon = Array.isArray(coupons) && coupons.length > 0 ? coupons[0] : null;
|
|
158
|
-
if (!coupon) {
|
|
159
|
-
return {
|
|
160
|
-
success: false,
|
|
161
|
-
errorCode: exports.ERROR_CODES.COUPON_NOT_FOUND,
|
|
162
|
-
message: exports.ERROR_MESSAGES.COUPON_NOT_FOUND,
|
|
163
|
-
};
|
|
164
|
-
}
|
|
165
|
-
// Validate phone number
|
|
166
|
-
if (!(0, validators_1.validatePhoneNumber)(phoneNumber)) {
|
|
167
|
-
return {
|
|
168
|
-
success: false,
|
|
169
|
-
errorCode: exports.ERROR_CODES.INVALID_REQUEST,
|
|
170
|
-
message: "Invalid phone number format",
|
|
171
|
-
};
|
|
172
|
-
}
|
|
173
|
-
// Perform all validations
|
|
174
|
-
if (!coupon.isActive) {
|
|
175
|
-
return {
|
|
176
|
-
success: false,
|
|
177
|
-
errorCode: exports.ERROR_CODES.COUPON_EXPIRED,
|
|
178
|
-
message: exports.ERROR_MESSAGES.COUPON_EXPIRED,
|
|
179
|
-
};
|
|
180
|
-
}
|
|
181
|
-
const now = new Date();
|
|
182
|
-
const validFrom = new Date(coupon.validFrom);
|
|
183
|
-
const validTo = new Date(coupon.validTo);
|
|
184
|
-
if (now < validFrom || now > validTo) {
|
|
185
|
-
return {
|
|
186
|
-
success: false,
|
|
187
|
-
errorCode: exports.ERROR_CODES.COUPON_EXPIRED,
|
|
188
|
-
message: exports.ERROR_MESSAGES.COUPON_EXPIRED,
|
|
189
|
-
};
|
|
190
|
-
}
|
|
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({
|
|
205
|
-
where: {
|
|
206
|
-
coupon: coupon.id,
|
|
207
|
-
phoneNumber,
|
|
208
|
-
},
|
|
209
|
-
});
|
|
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) {
|
|
216
|
-
return {
|
|
217
|
-
success: false,
|
|
218
|
-
errorCode: exports.ERROR_CODES.COUPON_ALREADY_USED,
|
|
219
|
-
message: exports.ERROR_MESSAGES.COUPON_ALREADY_USED,
|
|
220
|
-
};
|
|
221
|
-
}
|
|
222
|
-
// Check usage limit before attempting redemption
|
|
223
|
-
if (coupon.maxUsage && coupon.currentUsage >= coupon.maxUsage) {
|
|
224
|
-
return {
|
|
225
|
-
success: false,
|
|
226
|
-
errorCode: exports.ERROR_CODES.USAGE_LIMIT_REACHED,
|
|
227
|
-
message: exports.ERROR_MESSAGES.USAGE_LIMIT_REACHED,
|
|
228
|
-
};
|
|
229
|
-
}
|
|
230
|
-
// Calculate discount
|
|
231
|
-
let discountAmount = 0;
|
|
232
|
-
let finalAmount = orderAmount;
|
|
233
|
-
if (coupon.discountType === "percentage") {
|
|
234
|
-
discountAmount = (orderAmount * coupon.discountValue) / 100;
|
|
235
|
-
finalAmount = orderAmount - discountAmount;
|
|
236
|
-
}
|
|
237
|
-
else if (coupon.discountType === "flat") {
|
|
238
|
-
discountAmount = coupon.discountValue;
|
|
239
|
-
finalAmount = Math.max(0, orderAmount - discountAmount);
|
|
240
|
-
}
|
|
241
|
-
// Use transaction for atomic redemption
|
|
242
|
-
let redemptionId;
|
|
243
|
-
try {
|
|
244
|
-
// Atomic increment with optimistic locking
|
|
245
|
-
const updateResult = await strapi.db
|
|
246
|
-
.query("plugin::coupon.coupon")
|
|
247
|
-
.update({
|
|
248
|
-
where: {
|
|
249
|
-
id: coupon.id,
|
|
250
|
-
currentUsage: coupon.currentUsage, // Optimistic lock
|
|
251
|
-
},
|
|
252
|
-
data: {
|
|
253
|
-
currentUsage: coupon.currentUsage + 1,
|
|
254
|
-
},
|
|
255
|
-
});
|
|
256
|
-
// If update failed due to concurrent modification, check usage limit
|
|
257
|
-
if (!updateResult ||
|
|
258
|
-
(coupon.maxUsage && coupon.currentUsage + 1 > coupon.maxUsage)) {
|
|
259
|
-
return {
|
|
260
|
-
success: false,
|
|
261
|
-
errorCode: exports.ERROR_CODES.USAGE_LIMIT_REACHED,
|
|
262
|
-
message: exports.ERROR_MESSAGES.USAGE_LIMIT_REACHED,
|
|
263
|
-
};
|
|
264
|
-
}
|
|
265
|
-
// Create redemption record
|
|
266
|
-
const redemption = await strapi.db
|
|
267
|
-
.query("plugin::coupon.redemption")
|
|
268
|
-
.create({
|
|
269
|
-
data: {
|
|
270
|
-
coupon: coupon.id,
|
|
271
|
-
phoneNumber,
|
|
272
|
-
orderId,
|
|
273
|
-
discountType: coupon.discountType,
|
|
274
|
-
discountValue: coupon.discountValue,
|
|
275
|
-
redemptionDate: new Date(),
|
|
276
|
-
metadata: {
|
|
277
|
-
orderAmount,
|
|
278
|
-
finalAmount,
|
|
279
|
-
discountAmount,
|
|
280
|
-
},
|
|
281
|
-
},
|
|
282
|
-
});
|
|
283
|
-
redemptionId = `redeem_${redemption.id}`;
|
|
284
|
-
}
|
|
285
|
-
catch (error) {
|
|
286
|
-
strapi.log.error("Transaction error during redemption:", error);
|
|
287
|
-
throw error;
|
|
288
|
-
}
|
|
289
|
-
return {
|
|
290
|
-
success: true,
|
|
291
|
-
message: "Coupon redeemed successfully.",
|
|
292
|
-
redemptionId,
|
|
293
|
-
};
|
|
294
|
-
}
|
|
295
|
-
catch (error) {
|
|
296
|
-
strapi.log.error("Error redeeming coupon:", error);
|
|
297
|
-
throw error;
|
|
298
|
-
}
|
|
299
|
-
},
|
|
300
|
-
/**
|
|
301
|
-
* Create a new coupon (Admin only)
|
|
302
|
-
*/
|
|
303
|
-
async create(params) {
|
|
304
|
-
try {
|
|
305
|
-
const { code, discountType, discountValue, maxUsage, maxUsagePerUser, validFrom, validTo, description, userRestrictions, } = params;
|
|
306
|
-
// Validate coupon code
|
|
307
|
-
if (!(0, validators_1.validateCouponCode)(code)) {
|
|
308
|
-
return {
|
|
309
|
-
success: false,
|
|
310
|
-
errorCode: "INVALID_COUPON_CODE",
|
|
311
|
-
message: "Coupon code must be 3-50 uppercase alphanumeric characters",
|
|
312
|
-
};
|
|
313
|
-
}
|
|
314
|
-
// Validate discount value
|
|
315
|
-
const discountValidation = (0, validators_1.validateDiscountValue)(discountType, discountValue);
|
|
316
|
-
if (!discountValidation.valid) {
|
|
317
|
-
return {
|
|
318
|
-
success: false,
|
|
319
|
-
errorCode: "INVALID_DISCOUNT",
|
|
320
|
-
message: discountValidation.error || "Invalid discount value",
|
|
321
|
-
};
|
|
322
|
-
}
|
|
323
|
-
// Validate date range
|
|
324
|
-
const dateValidation = (0, validators_1.validateDateRange)(validFrom, validTo);
|
|
325
|
-
if (!dateValidation.valid) {
|
|
326
|
-
return {
|
|
327
|
-
success: false,
|
|
328
|
-
errorCode: "INVALID_DATE_RANGE",
|
|
329
|
-
message: dateValidation.error || "Invalid date range",
|
|
330
|
-
};
|
|
331
|
-
}
|
|
332
|
-
// Validate maxUsage
|
|
333
|
-
if (maxUsage !== undefined &&
|
|
334
|
-
(typeof maxUsage !== "number" || maxUsage < 1)) {
|
|
335
|
-
return {
|
|
336
|
-
success: false,
|
|
337
|
-
errorCode: "INVALID_MAX_USAGE",
|
|
338
|
-
message: "maxUsage must be a positive number",
|
|
339
|
-
};
|
|
340
|
-
}
|
|
341
|
-
// Check if coupon code already exists
|
|
342
|
-
const existingCoupons = await strapi.entityService.findMany("plugin::coupon.coupon", {
|
|
343
|
-
filters: {
|
|
344
|
-
code,
|
|
345
|
-
},
|
|
346
|
-
});
|
|
347
|
-
if (Array.isArray(existingCoupons) && existingCoupons.length > 0) {
|
|
348
|
-
return {
|
|
349
|
-
success: false,
|
|
350
|
-
errorCode: "COUPON_ALREADY_EXISTS",
|
|
351
|
-
message: "A coupon with this code already exists.",
|
|
352
|
-
};
|
|
353
|
-
}
|
|
354
|
-
// Create the coupon
|
|
355
|
-
const coupon = await strapi.entityService.create("plugin::coupon.coupon", {
|
|
356
|
-
data: {
|
|
357
|
-
code,
|
|
358
|
-
discountType,
|
|
359
|
-
discountValue,
|
|
360
|
-
maxUsage: maxUsage || null,
|
|
361
|
-
maxUsagePerUser: maxUsagePerUser ?? null,
|
|
362
|
-
currentUsage: 0,
|
|
363
|
-
validFrom,
|
|
364
|
-
validTo,
|
|
365
|
-
isActive: true,
|
|
366
|
-
description: description || null,
|
|
367
|
-
userRestrictions: userRestrictions || null,
|
|
368
|
-
},
|
|
369
|
-
});
|
|
370
|
-
return {
|
|
371
|
-
success: true,
|
|
372
|
-
couponId: `coupon_${coupon.id}`,
|
|
373
|
-
message: "Coupon created successfully.",
|
|
374
|
-
};
|
|
375
|
-
}
|
|
376
|
-
catch (error) {
|
|
377
|
-
strapi.log.error("Error creating coupon:", error);
|
|
378
|
-
throw error;
|
|
379
|
-
}
|
|
380
|
-
},
|
|
381
|
-
/**
|
|
382
|
-
* Get all coupons
|
|
383
|
-
*/
|
|
384
|
-
async findAll(query = {}) {
|
|
385
|
-
try {
|
|
386
|
-
return await strapi.entityService.findMany("plugin::coupon.coupon", query);
|
|
387
|
-
}
|
|
388
|
-
catch (error) {
|
|
389
|
-
strapi.log.error("Error finding coupons:", error);
|
|
390
|
-
throw error;
|
|
391
|
-
}
|
|
392
|
-
},
|
|
393
|
-
/**
|
|
394
|
-
* Get a single coupon
|
|
395
|
-
*/
|
|
396
|
-
async findOne(id) {
|
|
397
|
-
try {
|
|
398
|
-
return await strapi.entityService.findOne("plugin::coupon.coupon", id);
|
|
399
|
-
}
|
|
400
|
-
catch (error) {
|
|
401
|
-
strapi.log.error("Error finding coupon:", error);
|
|
402
|
-
throw error;
|
|
403
|
-
}
|
|
404
|
-
},
|
|
405
|
-
/**
|
|
406
|
-
* Update a coupon
|
|
407
|
-
*/
|
|
408
|
-
async update(id, data) {
|
|
409
|
-
try {
|
|
410
|
-
return await strapi.entityService.update("plugin::coupon.coupon", id, {
|
|
411
|
-
data,
|
|
412
|
-
});
|
|
413
|
-
}
|
|
414
|
-
catch (error) {
|
|
415
|
-
strapi.log.error("Error updating coupon:", error);
|
|
416
|
-
throw error;
|
|
417
|
-
}
|
|
418
|
-
},
|
|
419
|
-
/**
|
|
420
|
-
* Delete a coupon
|
|
421
|
-
*/
|
|
422
|
-
async delete(id) {
|
|
423
|
-
try {
|
|
424
|
-
return await strapi.entityService.delete("plugin::coupon.coupon", id);
|
|
425
|
-
}
|
|
426
|
-
catch (error) {
|
|
427
|
-
strapi.log.error("Error deleting coupon:", error);
|
|
428
|
-
throw error;
|
|
429
|
-
}
|
|
430
|
-
},
|
|
431
|
-
});
|
|
432
|
-
exports.default = couponService;
|