3a-ecommerce-utils 1.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.
Files changed (45) hide show
  1. package/README.md +163 -0
  2. package/dist/chunk-PEAZVBSD.mjs +597 -0
  3. package/dist/client-DYGi_pyp.d.mts +87 -0
  4. package/dist/client-DYGi_pyp.d.ts +87 -0
  5. package/dist/index.d.mts +496 -0
  6. package/dist/index.d.ts +496 -0
  7. package/dist/index.js +17707 -0
  8. package/dist/index.mjs +17043 -0
  9. package/dist/validation/server.d.mts +50 -0
  10. package/dist/validation/server.d.ts +50 -0
  11. package/dist/validation/server.js +518 -0
  12. package/dist/validation/server.mjs +168 -0
  13. package/package.json +69 -0
  14. package/src/api/address.queries.ts +96 -0
  15. package/src/api/category.queries.ts +85 -0
  16. package/src/api/coupon.queries.ts +120 -0
  17. package/src/api/dashboard.queries.ts +35 -0
  18. package/src/api/errorHandler.ts +164 -0
  19. package/src/api/graphqlClient.ts +113 -0
  20. package/src/api/index.ts +10 -0
  21. package/src/api/logger.client.ts +89 -0
  22. package/src/api/logger.ts +135 -0
  23. package/src/api/order.queries.ts +211 -0
  24. package/src/api/product.queries.ts +144 -0
  25. package/src/api/review.queries.ts +56 -0
  26. package/src/api/user.queries.ts +232 -0
  27. package/src/assets/3A.png +0 -0
  28. package/src/assets/index.ts +1 -0
  29. package/src/assets.d.ts +29 -0
  30. package/src/auth.ts +176 -0
  31. package/src/config/jest.backend.config.js +42 -0
  32. package/src/config/jest.frontend.config.js +50 -0
  33. package/src/config/postcss.config.js +6 -0
  34. package/src/config/tailwind.config.ts +70 -0
  35. package/src/config/tsconfig.base.json +36 -0
  36. package/src/config/vite.config.ts +86 -0
  37. package/src/config/vitest.base.config.ts +74 -0
  38. package/src/config/webpack.base.config.ts +126 -0
  39. package/src/constants/index.ts +312 -0
  40. package/src/cookies.ts +104 -0
  41. package/src/helpers.ts +400 -0
  42. package/src/index.ts +32 -0
  43. package/src/validation/client.ts +287 -0
  44. package/src/validation/index.ts +3 -0
  45. package/src/validation/server.ts +32 -0
package/src/helpers.ts ADDED
@@ -0,0 +1,400 @@
1
+ /**
2
+ * Helper Utilities
3
+ * Commonly used utility functions across services and frontend apps
4
+ */
5
+
6
+ export const capitalize = (str: string): string => {
7
+ if (!str) return '';
8
+ return str.charAt(0).toUpperCase() + str.slice(1);
9
+ };
10
+
11
+ export const capitalizeWords = (str: string): string => {
12
+ if (!str) return '';
13
+ return str
14
+ .split(' ')
15
+ .map((word) => capitalize(word))
16
+ .join(' ');
17
+ };
18
+
19
+ export const toTitleCase = (str: string): string => {
20
+ if (!str) return '';
21
+ return str.replace(/\w\S*/g, (txt) => txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase());
22
+ };
23
+
24
+ export const slugify = (str: string): string => {
25
+ if (!str) return '';
26
+ return str
27
+ .toLowerCase()
28
+ .trim()
29
+ .replace(/\s+/g, '-')
30
+ .replace(/[^\w-]/g, '');
31
+ };
32
+
33
+ export const generateRandomCode = (length: number = 6): string => {
34
+ const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
35
+ let result = '';
36
+ for (let i = 0; i < length; i++) {
37
+ result += chars.charAt(Math.floor(Math.random() * chars.length));
38
+ }
39
+ return result;
40
+ };
41
+
42
+ export const truncate = (str: string, length: number, suffix: string = '...'): string => {
43
+ if (!str || str.length <= length) return str;
44
+ return str.substring(0, length) + suffix;
45
+ };
46
+
47
+ // ============================================================================
48
+ // NUMBER UTILITIES
49
+ // ============================================================================
50
+
51
+ export const formatCurrency = (amount: number, currency: string = 'USD'): string => {
52
+ return new Intl.NumberFormat('en-US', {
53
+ style: 'currency',
54
+ currency: currency,
55
+ }).format(amount);
56
+ };
57
+
58
+ export const formatNumber = (num: number, decimals: number = 2): number => {
59
+ return parseFloat(num.toFixed(decimals));
60
+ };
61
+
62
+ export const roundUp = (num: number, decimals: number = 2): number => {
63
+ const factor = Math.pow(10, decimals);
64
+ return Math.ceil(num * factor) / factor;
65
+ };
66
+
67
+ export const roundDown = (num: number, decimals: number = 2): number => {
68
+ const factor = Math.pow(10, decimals);
69
+ return Math.floor(num * factor) / factor;
70
+ };
71
+
72
+ export const calculatePercentage = (value: number, percentage: number): number => {
73
+ return formatNumber((value * percentage) / 100);
74
+ };
75
+
76
+ export const calculateDiscount = (price: number, discountPercent: number): number => {
77
+ return formatNumber(price - (price * discountPercent) / 100);
78
+ };
79
+
80
+ export const calculateTax = (amount: number, taxRate: number): number => {
81
+ return formatNumber(amount * taxRate);
82
+ };
83
+
84
+ // ============================================================================
85
+ // ARRAY UTILITIES
86
+ // ============================================================================
87
+
88
+ export const chunk = <T>(arr: T[], size: number): T[][] => {
89
+ const chunks: T[][] = [];
90
+ for (let i = 0; i < arr.length; i += size) {
91
+ chunks.push(arr.slice(i, i + size));
92
+ }
93
+ return chunks;
94
+ };
95
+
96
+ export const unique = <T>(arr: T[]): T[] => {
97
+ return Array.from(new Set(arr));
98
+ };
99
+
100
+ export const removeDuplicates = <T extends Record<string, any>>(arr: T[], key: keyof T): T[] => {
101
+ const seen = new Set();
102
+ return arr.filter((item) => {
103
+ const value = item[key];
104
+ if (seen.has(value)) return false;
105
+ seen.add(value);
106
+ return true;
107
+ });
108
+ };
109
+
110
+ export const groupBy = <T extends Record<string, any>>(arr: T[], key: keyof T) => {
111
+ return arr.reduce((result, item) => {
112
+ const groupKey = String(item[key]);
113
+ if (!result[groupKey]) {
114
+ result[groupKey] = [];
115
+ }
116
+ result[groupKey].push(item);
117
+ return result;
118
+ }, {} as Record<string, T[]>);
119
+ };
120
+
121
+ export const sortBy = <T extends Record<string, any>>(
122
+ arr: T[],
123
+ key: keyof T,
124
+ order: 'ASC' | 'DESC' = 'ASC'
125
+ ): T[] => {
126
+ const sorted = [...arr].sort((a, b) => {
127
+ const aVal = a[key];
128
+ const bVal = b[key];
129
+ if (aVal < bVal) return order === 'ASC' ? -1 : 1;
130
+ if (aVal > bVal) return order === 'ASC' ? 1 : -1;
131
+ return 0;
132
+ });
133
+ return sorted;
134
+ };
135
+
136
+ export const flatten = <T>(arr: T[][]): T[] => {
137
+ return arr.reduce((flat, item) => flat.concat(item), []);
138
+ };
139
+
140
+ export const difference = <T>(arr1: T[], arr2: T[]): T[] => {
141
+ return arr1.filter((item) => !arr2.includes(item));
142
+ };
143
+
144
+ export const intersection = <T>(arr1: T[], arr2: T[]): T[] => {
145
+ return arr1.filter((item) => arr2.includes(item));
146
+ };
147
+
148
+ // ============================================================================
149
+ // OBJECT UTILITIES
150
+ // ============================================================================
151
+
152
+ export const omit = <T extends Record<string, any>>(obj: T, keys: (keyof T)[]): Partial<T> => {
153
+ const result = { ...obj };
154
+ keys.forEach((key) => delete result[key]);
155
+ return result;
156
+ };
157
+
158
+ export const pick = <T extends Record<string, any>>(obj: T, keys: (keyof T)[]): Partial<T> => {
159
+ const result: Partial<T> = {};
160
+ keys.forEach((key) => {
161
+ if (key in obj) {
162
+ result[key] = obj[key];
163
+ }
164
+ });
165
+ return result;
166
+ };
167
+
168
+ export const deepMerge = <T extends Record<string, any>>(target: T, source: Partial<T>): T => {
169
+ const result = { ...target } as T;
170
+ for (const key in source) {
171
+ if (source.hasOwnProperty(key)) {
172
+ if (typeof source[key] === 'object' && source[key] !== null && !Array.isArray(source[key])) {
173
+ result[key] = deepMerge(
174
+ (target[key] as Record<string, any>) || {},
175
+ source[key] as Record<string, any>
176
+ ) as T[Extract<keyof T, string>];
177
+ } else {
178
+ result[key] = source[key] as T[Extract<keyof T, string>];
179
+ }
180
+ }
181
+ }
182
+ return result;
183
+ };
184
+
185
+ export const isEmpty = (obj: Record<string, any>): boolean => {
186
+ return Object.keys(obj).length === 0;
187
+ };
188
+
189
+ export const getNestedValue = <T = any>(obj: Record<string, any>, path: string): T | undefined => {
190
+ const keys = path.split('.');
191
+ let result: any = obj;
192
+ for (const key of keys) {
193
+ if (result && typeof result === 'object' && key in result) {
194
+ result = result[key];
195
+ } else {
196
+ return undefined;
197
+ }
198
+ }
199
+ return result;
200
+ };
201
+
202
+ export const setNestedValue = (obj: Record<string, any>, path: string, value: any): void => {
203
+ const keys = path.split('.');
204
+ let current = obj;
205
+ for (let i = 0; i < keys.length - 1; i++) {
206
+ const key = keys[i];
207
+ if (!(key in current) || typeof current[key] !== 'object') {
208
+ current[key] = {};
209
+ }
210
+ current = current[key];
211
+ }
212
+ current[keys[keys.length - 1]] = value;
213
+ };
214
+
215
+ // ============================================================================
216
+ // DATE UTILITIES
217
+ // ============================================================================
218
+
219
+ export const formatDate = (date: Date | string, format: string = 'MM/DD/YYYY'): string => {
220
+ const dateObj = new Date(date);
221
+ const year = dateObj.getFullYear();
222
+ const month = String(dateObj.getMonth() + 1).padStart(2, '0');
223
+ const day = String(dateObj.getDate()).padStart(2, '0');
224
+ const hours = String(dateObj.getHours()).padStart(2, '0');
225
+ const minutes = String(dateObj.getMinutes()).padStart(2, '0');
226
+ const seconds = String(dateObj.getSeconds()).padStart(2, '0');
227
+
228
+ return format
229
+ .replace('YYYY', String(year))
230
+ .replace('MM', month)
231
+ .replace('DD', day)
232
+ .replace('HH', hours)
233
+ .replace('mm', minutes)
234
+ .replace('ss', seconds);
235
+ };
236
+
237
+ export const addDays = (date: Date, days: number): Date => {
238
+ const result = new Date(date);
239
+ result.setDate(result.getDate() + days);
240
+ return result;
241
+ };
242
+
243
+ export const addHours = (date: Date, hours: number): Date => {
244
+ const result = new Date(date);
245
+ result.setHours(result.getHours() + hours);
246
+ return result;
247
+ };
248
+
249
+ export const getDaysDifference = (date1: Date, date2: Date): number => {
250
+ const diffTime = Math.abs(date2.getTime() - date1.getTime());
251
+ return Math.ceil(diffTime / (1000 * 60 * 60 * 24));
252
+ };
253
+
254
+ export const isDateInRange = (date: Date, startDate: Date, endDate: Date): boolean => {
255
+ return date >= startDate && date <= endDate;
256
+ };
257
+
258
+ export const isFutureDate = (date: Date): boolean => {
259
+ return date > new Date();
260
+ };
261
+
262
+ export const isPastDate = (date: Date): boolean => {
263
+ return date < new Date();
264
+ };
265
+
266
+ // ============================================================================
267
+ // DELAY & TIMING UTILITIES
268
+ // ============================================================================
269
+
270
+ export const delay = (ms: number): Promise<void> => {
271
+ return new Promise((resolve) => setTimeout(resolve, ms));
272
+ };
273
+
274
+ export const debounce = <T extends (...args: any[]) => any>(
275
+ fn: T,
276
+ wait: number
277
+ ): ((...args: Parameters<T>) => void) => {
278
+ let timeoutId: ReturnType<typeof setTimeout>;
279
+ return (...args: Parameters<T>) => {
280
+ clearTimeout(timeoutId);
281
+ timeoutId = setTimeout(() => fn(...args), wait);
282
+ };
283
+ };
284
+
285
+ export const throttle = <T extends (...args: any[]) => any>(
286
+ fn: T,
287
+ limit: number
288
+ ): ((...args: Parameters<T>) => void) => {
289
+ let inThrottle: boolean;
290
+ return (...args: Parameters<T>) => {
291
+ if (!inThrottle) {
292
+ fn(...args);
293
+ inThrottle = true;
294
+ setTimeout(() => (inThrottle = false), limit);
295
+ }
296
+ };
297
+ };
298
+
299
+ // ============================================================================
300
+ // ERROR HANDLING UTILITIES
301
+ // ============================================================================
302
+
303
+ export const tryParse = <T = any>(json: string, fallback: T): T => {
304
+ try {
305
+ return JSON.parse(json) as T;
306
+ } catch {
307
+ return fallback;
308
+ }
309
+ };
310
+
311
+ export const withErrorHandling = async <T>(
312
+ fn: () => Promise<T>
313
+ ): Promise<{ success: boolean; data?: T; error?: string }> => {
314
+ try {
315
+ const data = await fn();
316
+ return { success: true, data };
317
+ } catch (error) {
318
+ const message = error instanceof Error ? error.message : 'Unknown error';
319
+ return { success: false, error: message };
320
+ }
321
+ };
322
+
323
+ export const createApiResponse = <T>(
324
+ success: boolean,
325
+ data?: T,
326
+ message?: string,
327
+ statusCode: number = 200
328
+ ) => {
329
+ return {
330
+ success,
331
+ statusCode,
332
+ message: message || (success ? 'Success' : 'Error'),
333
+ data: data || (success ? {} : null),
334
+ };
335
+ };
336
+
337
+ // ============================================================================
338
+ // RETRY UTILITIES
339
+ // ============================================================================
340
+
341
+ export const retry = async <T>(
342
+ fn: () => Promise<T>,
343
+ maxAttempts: number = 3,
344
+ delayMs: number = 1000
345
+ ): Promise<T> => {
346
+ for (let attempt = 1; attempt <= maxAttempts; attempt++) {
347
+ try {
348
+ return await fn();
349
+ } catch (error) {
350
+ if (attempt === maxAttempts) throw error;
351
+ await delay(delayMs * attempt);
352
+ }
353
+ }
354
+ throw new Error('Retry failed');
355
+ };
356
+
357
+ // ============================================================================
358
+ // TYPE CHECKING UTILITIES
359
+ // ============================================================================
360
+
361
+ export const isString = (val: unknown): val is string => typeof val === 'string';
362
+ export const isNumber = (val: unknown): val is number => typeof val === 'number';
363
+ export const isBoolean = (val: unknown): val is boolean => typeof val === 'boolean';
364
+ export const isArray = (val: unknown): val is unknown[] => Array.isArray(val);
365
+ export const isObject = (val: unknown): val is Record<string, unknown> =>
366
+ typeof val === 'object' && val !== null && !Array.isArray(val);
367
+ export const isFunction = (val: unknown): val is Function => typeof val === 'function';
368
+ export const isNull = (val: unknown): val is null => val === null;
369
+ export const isUndefined = (val: unknown): val is undefined => val === undefined;
370
+ export const isNullOrUndefined = (val: unknown): val is null | undefined =>
371
+ val === null || val === undefined;
372
+
373
+ export const formatIndianCompact = (num: number = 0) => {
374
+ if (num >= 1e7) {
375
+ return (num / 1e7).toFixed(2).replace(/\.00$/, '') + 'Cr';
376
+ }
377
+ if (num >= 1e5) {
378
+ return (num / 1e5).toFixed(2).replace(/\.00$/, '') + 'L';
379
+ }
380
+ return (
381
+ '₹' +
382
+ num?.toLocaleString('en-IN', {
383
+ minimumFractionDigits: 2,
384
+ maximumFractionDigits: 2,
385
+ })
386
+ );
387
+ };
388
+
389
+ export function formatPrice(price: number): string {
390
+ return `₹${price?.toFixed(2)}`;
391
+ }
392
+
393
+ export function formatPriceShort(price: number): string {
394
+ if (price >= 100000) {
395
+ return `₹${(price / 100000).toFixed(1)}L`;
396
+ } else if (price >= 1000) {
397
+ return `₹${(price / 1000).toFixed(1)}K`;
398
+ }
399
+ return `₹${price.toFixed(0)}`;
400
+ }
package/src/index.ts ADDED
@@ -0,0 +1,32 @@
1
+
2
+ export {
3
+ storeAuth,
4
+ getStoredAuth,
5
+ isTokenExpired,
6
+ willTokenExpireSoon,
7
+ clearAuth,
8
+ validateUserRole,
9
+ getCurrentUser,
10
+ getAccessToken,
11
+ getRefreshToken,
12
+ updateAccessToken,
13
+ setupAutoRefresh,
14
+ type AuthTokens,
15
+ type StoredAuth,
16
+ } from './auth';
17
+
18
+ export {
19
+ setCookie,
20
+ getCookie,
21
+ removeCookie,
22
+ areCookiesEnabled,
23
+ AUTH_COOKIE_NAMES,
24
+ } from './cookies';
25
+
26
+ export * from './api';
27
+ export * from './helpers';
28
+ export * from './constants';
29
+ export * from './validation';
30
+
31
+
32
+
@@ -0,0 +1,287 @@
1
+ /**
2
+ * Client-safe validation utilities
3
+ * These can be used in both frontend and backend
4
+ */
5
+
6
+ import { REGEX_PATTERNS, JWT_CONFIG } from '../constants';
7
+
8
+ // EMAIL VALIDATION
9
+ export const isValidEmail = (email: string): boolean => {
10
+ return REGEX_PATTERNS.EMAIL.test(email);
11
+ };
12
+
13
+ export const validateEmail = (email: string): { valid: boolean; error?: string } => {
14
+ if (!email || email.trim() === '') {
15
+ return { valid: false, error: 'Email is required' };
16
+ }
17
+ if (!isValidEmail(email)) {
18
+ return { valid: false, error: 'Invalid email format' };
19
+ }
20
+ if (email.length > 255) {
21
+ return { valid: false, error: 'Email is too long' };
22
+ }
23
+ return { valid: true };
24
+ };
25
+
26
+ // PASSWORD VALIDATION
27
+ export const isValidPassword = (password: string): boolean => {
28
+ return JWT_CONFIG.PASSWORD_PATTERN.test(password);
29
+ };
30
+
31
+ export const validatePassword = (password: string): { valid: boolean; error?: string } => {
32
+ if (!password) {
33
+ return { valid: false, error: 'Password is required' };
34
+ }
35
+ if (password.length < JWT_CONFIG.PASSWORD_MIN_LENGTH) {
36
+ return {
37
+ valid: false,
38
+ error: `Password must be at least ${JWT_CONFIG.PASSWORD_MIN_LENGTH} characters`,
39
+ };
40
+ }
41
+ if (!isValidPassword(password)) {
42
+ return {
43
+ valid: false,
44
+ error: 'Password must contain uppercase, lowercase, number, and special character (@$!%*?&)',
45
+ };
46
+ }
47
+ return { valid: true };
48
+ };
49
+
50
+ // USERNAME/NAME VALIDATION
51
+ export const validateName = (name: string): { valid: boolean; error?: string } => {
52
+ if (!name || name.trim() === '') {
53
+ return { valid: false, error: 'Name is required' };
54
+ }
55
+ if (name.length < 2) {
56
+ return { valid: false, error: 'Name must be at least 2 characters' };
57
+ }
58
+ if (name.length > 100) {
59
+ return { valid: false, error: 'Name is too long' };
60
+ }
61
+ // Check for valid characters (letters, spaces, hyphens, apostrophes)
62
+ if (!/^[a-zA-Z\s'-]+$/.test(name)) {
63
+ return {
64
+ valid: false,
65
+ error: 'Name can only contain letters, spaces, hyphens, and apostrophes',
66
+ };
67
+ }
68
+ return { valid: true };
69
+ };
70
+
71
+ // PHONE VALIDATION
72
+ export const isValidPhone = (phone: string): boolean => {
73
+ return REGEX_PATTERNS.PHONE.test(phone);
74
+ };
75
+
76
+ export const validatePhone = (phone: string): { valid: boolean; error?: string } => {
77
+ if (!phone) {
78
+ return { valid: false, error: 'Phone number is required' };
79
+ }
80
+ if (!isValidPhone(phone)) {
81
+ return { valid: false, error: 'Invalid phone number format' };
82
+ }
83
+ return { valid: true };
84
+ };
85
+
86
+ // POSTAL CODE VALIDATION
87
+ export const isValidPostalCode = (postalCode: string): boolean => {
88
+ return REGEX_PATTERNS.POSTAL_CODE.test(postalCode);
89
+ };
90
+
91
+ export const validatePostalCode = (postalCode: string): { valid: boolean; error?: string } => {
92
+ if (!postalCode) {
93
+ return { valid: false, error: 'Postal code is required' };
94
+ }
95
+ if (!isValidPostalCode(postalCode)) {
96
+ return { valid: false, error: 'Invalid postal code format' };
97
+ }
98
+ return { valid: true };
99
+ };
100
+
101
+ // COUPON CODE VALIDATION
102
+ export const isValidCouponCode = (code: string): boolean => {
103
+ return REGEX_PATTERNS.COUPON_CODE.test(code);
104
+ };
105
+
106
+ export const validateCouponCode = (code: string): { valid: boolean; error?: string } => {
107
+ if (!code) {
108
+ return { valid: false, error: 'Coupon code is required' };
109
+ }
110
+ if (!isValidCouponCode(code)) {
111
+ return { valid: false, error: 'Invalid coupon code format (6-20 alphanumeric characters)' };
112
+ }
113
+ return { valid: true };
114
+ };
115
+
116
+ // PRICE VALIDATION
117
+ export const validatePrice = (price: any): { valid: boolean; error?: string } => {
118
+ const numPrice = parseFloat(price);
119
+ if (isNaN(numPrice)) {
120
+ return { valid: false, error: 'Price must be a valid number' };
121
+ }
122
+ if (numPrice < 0) {
123
+ return { valid: false, error: 'Price cannot be negative' };
124
+ }
125
+ if (numPrice > 999999.99) {
126
+ return { valid: false, error: 'Price exceeds maximum allowed value' };
127
+ }
128
+ return { valid: true };
129
+ };
130
+
131
+ // QUANTITY/STOCK VALIDATION
132
+ export const validateQuantity = (qty: any): { valid: boolean; error?: string } => {
133
+ const numQty = parseInt(qty, 10);
134
+ if (isNaN(numQty)) {
135
+ return { valid: false, error: 'Quantity must be a valid number' };
136
+ }
137
+ if (numQty < 0) {
138
+ return { valid: false, error: 'Quantity cannot be negative' };
139
+ }
140
+ if (!Number.isInteger(numQty)) {
141
+ return { valid: false, error: 'Quantity must be a whole number' };
142
+ }
143
+ return { valid: true };
144
+ };
145
+
146
+ // RATING VALIDATION
147
+ export const validateRating = (rating: any): { valid: boolean; error?: string } => {
148
+ const numRating = parseFloat(rating);
149
+ if (isNaN(numRating)) {
150
+ return { valid: false, error: 'Rating must be a valid number' };
151
+ }
152
+ if (numRating < 0 || numRating > 5) {
153
+ return { valid: false, error: 'Rating must be between 0 and 5' };
154
+ }
155
+ return { valid: true };
156
+ };
157
+
158
+ // DISCOUNT VALIDATION
159
+ export const validateDiscountPercentage = (discount: any): { valid: boolean; error?: string } => {
160
+ const numDiscount = parseFloat(discount);
161
+ if (isNaN(numDiscount)) {
162
+ return { valid: false, error: 'Discount must be a valid number' };
163
+ }
164
+ if (numDiscount < 0 || numDiscount > 100) {
165
+ return { valid: false, error: 'Discount percentage must be between 0 and 100' };
166
+ }
167
+ return { valid: true };
168
+ };
169
+
170
+ // PAGINATION VALIDATION
171
+ export const validatePagination = (
172
+ page: any,
173
+ limit: any
174
+ ): { valid: boolean; error?: string; page?: number; limit?: number } => {
175
+ const numPage = parseInt(page, 10) || 1;
176
+ const numLimit = parseInt(limit, 10) || 10;
177
+
178
+ if (numPage < 1) {
179
+ return { valid: false, error: 'Page must be greater than 0' };
180
+ }
181
+ if (numLimit < 1) {
182
+ return { valid: false, error: 'Limit must be greater than 0' };
183
+ }
184
+ if (numLimit > 100) {
185
+ return { valid: false, error: 'Limit cannot exceed 100' };
186
+ }
187
+
188
+ return { valid: true, page: numPage, limit: numLimit };
189
+ };
190
+
191
+ // OBJECT ID VALIDATION (MongoDB ObjectId)
192
+ export const isValidObjectId = (id: string): boolean => {
193
+ return /^[0-9a-fA-F]{24}$/.test(id);
194
+ };
195
+
196
+ export const validateObjectId = (
197
+ id: string,
198
+ fieldName: string = 'ID'
199
+ ): { valid: boolean; error?: string } => {
200
+ if (!id) {
201
+ return { valid: false, error: `${fieldName} is required` };
202
+ }
203
+ if (!isValidObjectId(id)) {
204
+ return { valid: false, error: `Invalid ${fieldName} format` };
205
+ }
206
+ return { valid: true };
207
+ };
208
+
209
+ // URL VALIDATION
210
+ export const isValidUrl = (url: string): boolean => {
211
+ return REGEX_PATTERNS.URL.test(url);
212
+ };
213
+
214
+ export const validateUrl = (url: string): { valid: boolean; error?: string } => {
215
+ if (!url) {
216
+ return { valid: false, error: 'URL is required' };
217
+ }
218
+ if (!isValidUrl(url)) {
219
+ return { valid: false, error: 'Invalid URL format' };
220
+ }
221
+ return { valid: true };
222
+ };
223
+
224
+ // PRODUCT SKU VALIDATION
225
+ export const isValidSku = (sku: string): boolean => {
226
+ return REGEX_PATTERNS.PRODUCT_SKU.test(sku);
227
+ };
228
+
229
+ export const validateSku = (sku: string): { valid: boolean; error?: string } => {
230
+ if (!sku) {
231
+ return { valid: false, error: 'SKU is required' };
232
+ }
233
+ if (!isValidSku(sku)) {
234
+ return {
235
+ valid: false,
236
+ error: 'Invalid SKU format (3-20 alphanumeric characters with hyphens)',
237
+ };
238
+ }
239
+ return { valid: true };
240
+ };
241
+
242
+ // DATE VALIDATION
243
+ export const validateDate = (date: any): { valid: boolean; error?: string } => {
244
+ const dateObj = new Date(date);
245
+ if (isNaN(dateObj.getTime())) {
246
+ return { valid: false, error: 'Invalid date format' };
247
+ }
248
+ return { valid: true };
249
+ };
250
+
251
+ export const validateDateRange = (
252
+ startDate: any,
253
+ endDate: any
254
+ ): { valid: boolean; error?: string } => {
255
+ const start = new Date(startDate);
256
+ const end = new Date(endDate);
257
+
258
+ if (isNaN(start.getTime())) {
259
+ return { valid: false, error: 'Invalid start date' };
260
+ }
261
+ if (isNaN(end.getTime())) {
262
+ return { valid: false, error: 'Invalid end date' };
263
+ }
264
+ if (start > end) {
265
+ return { valid: false, error: 'Start date must be before end date' };
266
+ }
267
+
268
+ return { valid: true };
269
+ };
270
+
271
+ // BATCH VALIDATION HELPER
272
+ export const batchValidate = (
273
+ validations: Array<{ valid: boolean; error?: string }>
274
+ ): { valid: boolean; errors: string[] } => {
275
+ const errors: string[] = [];
276
+
277
+ for (const validation of validations) {
278
+ if (!validation.valid && validation.error) {
279
+ errors.push(validation.error);
280
+ }
281
+ }
282
+
283
+ return {
284
+ valid: errors.length === 0,
285
+ errors,
286
+ };
287
+ };