From edb525eb80646342952a5683d41c5d573434c49a Mon Sep 17 00:00:00 2001 From: Vaibhav Tupe Date: Tue, 10 Mar 2026 12:43:27 +0530 Subject: [PATCH] first commit --- .dockerignore | 4 + .env.example | 19 + .eslintrc.js | 37 + .gitignore | 132 + .prettierrc | 11 + CouponGuide.md | 586 + Dockerfile | 14 + Inventory.md | 332 + LOCAL_SETUP.md | 183 + README.md | 289 + package-lock.json | 9431 +++++++++++++++++ package.json | 69 + prisma/schema.prisma | 384 + prisma/seed.js | 174 + setup-local.js | 101 + src/config/database.js | 57 + src/config/returnPolicy.js | 5 + src/config/s3.js | 16 + src/controllers/admin/categoryController.js | 375 + src/controllers/admin/couponController.js | 463 + src/controllers/admin/dashboardController.js | 589 + src/controllers/admin/orderController.js | 395 + src/controllers/admin/productController.js | 782 ++ src/controllers/admin/reportController.js | 1170 ++ src/controllers/admin/userController.js | 160 + src/controllers/authController.js | 421 + src/controllers/couponController.js | 351 + src/controllers/orderController.js | 822 ++ src/controllers/orderTrackingController.js | 719 ++ src/controllers/payment/paytmController.js | 430 + src/controllers/products/productController.js | 811 ++ src/controllers/products/recommendation.js | 345 + src/controllers/users/addressController.js | 128 + src/controllers/users/adminUserController.js | 70 + src/controllers/users/cartController.js | 150 + src/controllers/users/orderController.js | 51 + src/controllers/users/profileController.js | 113 + src/controllers/users/wishlistController.js | 245 + .../wardrobe/wardrobeItemController.js | 200 + .../wardrobe/wardrobeMainController.js | 66 + .../wardrobe/wardrobePublicController.js | 48 + .../wardrobeRecommendationController.js | 29 + .../wardrobe/wardrobeSearchController.js | 52 + .../wardrobe/wardrobeStatsController.js | 39 + src/middleware/auth.js | 153 + src/middleware/errorHandler.js | 61 + src/middleware/upload.js | 10 + src/middleware/uploadProfile.js | 25 + src/models/mongodb/Product.js | 310 + src/models/mongodb/Wardrobe.js | 254 + src/routes/admin.js | 328 + src/routes/auth.js | 68 + src/routes/couponRoutes.js | 40 + src/routes/deliveryRoutes.js | 35 + src/routes/orders.js | 100 + src/routes/paymentRoutes.js | 52 + src/routes/products.js | 137 + src/routes/reports.js | 85 + src/routes/upload.routes.js | 17 + src/routes/users.js | 550 + src/routes/wardrobe.js | 393 + src/scripts/addStockFieldToProducts.js | 149 + src/scripts/migrateCategoryIds.js | 48 + src/server.js | 149 + src/services/authService.js | 348 + src/services/deliveryEstimationService.js | 266 + src/services/inventoryService.js | 264 + src/services/otpStore.js | 59 + src/services/s3Upload.service.js | 25 + src/services/wappconnectService.js | 63 + src/utils/mailer.js | 34 + src/utils/paytm.js | 315 + src/utils/uploadToS3.js | 23 + src/utils/whatsapp.js | 35 + src/views/emails/reset-password.ejs | 35 + src/views/emails/verify-email.ejs | 35 + structure.txt | Bin 0 -> 1220072 bytes test-connections.js | 119 + testWhatsApp.js | 191 + 79 files changed, 25644 insertions(+) create mode 100644 .dockerignore create mode 100644 .env.example create mode 100644 .eslintrc.js create mode 100644 .gitignore create mode 100644 .prettierrc create mode 100644 CouponGuide.md create mode 100644 Dockerfile create mode 100644 Inventory.md create mode 100644 LOCAL_SETUP.md create mode 100644 README.md create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 prisma/schema.prisma create mode 100644 prisma/seed.js create mode 100644 setup-local.js create mode 100644 src/config/database.js create mode 100644 src/config/returnPolicy.js create mode 100644 src/config/s3.js create mode 100644 src/controllers/admin/categoryController.js create mode 100644 src/controllers/admin/couponController.js create mode 100644 src/controllers/admin/dashboardController.js create mode 100644 src/controllers/admin/orderController.js create mode 100644 src/controllers/admin/productController.js create mode 100644 src/controllers/admin/reportController.js create mode 100644 src/controllers/admin/userController.js create mode 100644 src/controllers/authController.js create mode 100644 src/controllers/couponController.js create mode 100644 src/controllers/orderController.js create mode 100644 src/controllers/orderTrackingController.js create mode 100644 src/controllers/payment/paytmController.js create mode 100644 src/controllers/products/productController.js create mode 100644 src/controllers/products/recommendation.js create mode 100644 src/controllers/users/addressController.js create mode 100644 src/controllers/users/adminUserController.js create mode 100644 src/controllers/users/cartController.js create mode 100644 src/controllers/users/orderController.js create mode 100644 src/controllers/users/profileController.js create mode 100644 src/controllers/users/wishlistController.js create mode 100644 src/controllers/wardrobe/wardrobeItemController.js create mode 100644 src/controllers/wardrobe/wardrobeMainController.js create mode 100644 src/controllers/wardrobe/wardrobePublicController.js create mode 100644 src/controllers/wardrobe/wardrobeRecommendationController.js create mode 100644 src/controllers/wardrobe/wardrobeSearchController.js create mode 100644 src/controllers/wardrobe/wardrobeStatsController.js create mode 100644 src/middleware/auth.js create mode 100644 src/middleware/errorHandler.js create mode 100644 src/middleware/upload.js create mode 100644 src/middleware/uploadProfile.js create mode 100644 src/models/mongodb/Product.js create mode 100644 src/models/mongodb/Wardrobe.js create mode 100644 src/routes/admin.js create mode 100644 src/routes/auth.js create mode 100644 src/routes/couponRoutes.js create mode 100644 src/routes/deliveryRoutes.js create mode 100644 src/routes/orders.js create mode 100644 src/routes/paymentRoutes.js create mode 100644 src/routes/products.js create mode 100644 src/routes/reports.js create mode 100644 src/routes/upload.routes.js create mode 100644 src/routes/users.js create mode 100644 src/routes/wardrobe.js create mode 100644 src/scripts/addStockFieldToProducts.js create mode 100644 src/scripts/migrateCategoryIds.js create mode 100644 src/server.js create mode 100644 src/services/authService.js create mode 100644 src/services/deliveryEstimationService.js create mode 100644 src/services/inventoryService.js create mode 100644 src/services/otpStore.js create mode 100644 src/services/s3Upload.service.js create mode 100644 src/services/wappconnectService.js create mode 100644 src/utils/mailer.js create mode 100644 src/utils/paytm.js create mode 100644 src/utils/uploadToS3.js create mode 100644 src/utils/whatsapp.js create mode 100644 src/views/emails/reset-password.ejs create mode 100644 src/views/emails/verify-email.ejs create mode 100644 structure.txt create mode 100644 test-connections.js create mode 100644 testWhatsApp.js diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..b8b83d3 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,4 @@ +node_modules +npm-debug.log +.git +.env diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..6629524 --- /dev/null +++ b/.env.example @@ -0,0 +1,19 @@ +# Server Configuration +NODE_ENV=development +PORT=3000 +HOST=localhost + +# Database Configuration +DATABASE_URL="postgresql://username:password@localhost:5432/vaishnavi_db?schema=public" +MONGODB_URI="mongodb://localhost:27017/vaishnavi_products" +REDIS_URL="redis://localhost:6379" + +# JWT Configuration +JWT_SECRET="your-super-secret-jwt-key-change-this-in-production" +JWT_EXPIRES_IN="7d" + +# AWS S3 Configuration +AWS_ACCESS_KEY_ID="your-aws-access-key" +AWS_SECRET_ACCESS_KEY="your-aws-secret-key" +AWS_REGION="us-east-1" +AWS_S3_BUCKET="vaishnavi-files" diff --git a/.eslintrc.js b/.eslintrc.js new file mode 100644 index 0000000..6474ab1 --- /dev/null +++ b/.eslintrc.js @@ -0,0 +1,37 @@ +module.exports = { + env: { + node: true, + es2021: true, + jest: true, + }, + extends: [ + 'eslint:recommended', + ], + parserOptions: { + ecmaVersion: 2021, + sourceType: 'module', + }, + rules: { + 'no-console': 'warn', + 'no-unused-vars': ['error', { argsIgnorePattern: '^_' }], + 'prefer-const': 'error', + 'no-var': 'error', + 'object-shorthand': 'error', + 'prefer-template': 'error', + 'template-curly-spacing': 'error', + 'arrow-spacing': 'error', + 'comma-dangle': ['error', 'always-multiline'], + 'quotes': ['error', 'single', { avoidEscape: true }], + 'semi': ['error', 'always'], + 'indent': ['error', 2], + 'no-trailing-spaces': 'error', + 'eol-last': 'error', + }, + ignorePatterns: [ + 'node_modules/', + 'dist/', + 'build/', + 'coverage/', + '*.min.js', + ], +}; diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..dd378e7 --- /dev/null +++ b/.gitignore @@ -0,0 +1,132 @@ +# Dependencies +node_modules/ +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# Environment variables +.env +.env.local +.env.development.local +.env.test.local +.env.production.local + +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +lerna-debug.log* + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Coverage directory used by tools like istanbul +coverage/ +*.lcov + +# nyc test coverage +.nyc_output + +# Grunt intermediate storage +.grunt + +# Bower dependency directory +bower_components + +# node-waf configuration +.lock-wscript + +# Compiled binary addons +build/Release + +# Dependency directories +node_modules/ +jspm_packages/ + +# TypeScript cache +*.tsbuildinfo + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Microbundle cache +.rpt2_cache/ +.rts2_cache_cjs/ +.rts2_cache_es/ +.rts2_cache_umd/ + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# dotenv environment variables file +.env +.env.test + +# parcel-bundler cache +.cache +.parcel-cache + +# Next.js build output +.next + +# Nuxt.js build / generate output +.nuxt +dist + +# Gatsby files +.cache/ +public + +# Storybook build outputs +.out +.storybook-out + +# Temporary folders +tmp/ +temp/ + +# Editor directories and files +.vscode/ +.idea/ +*.swp +*.swo +*~ + +# OS generated files +.DS_Store +.DS_Store? +._* +.Spotlight-V100 +.Trashes +ehthumbs.db +Thumbs.db + +# Prisma +prisma/migrations/ + +# Uploads (if storing locally) +uploads/ +public/uploads/ + +# Database +*.sqlite +*.db + +/src/generated/prisma + + +uploads/ diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..b1c7fb5 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,11 @@ +{ + "semi": true, + "trailingComma": "es5", + "singleQuote": true, + "printWidth": 80, + "tabWidth": 2, + "useTabs": false, + "bracketSpacing": true, + "arrowParens": "avoid", + "endOfLine": "lf" +} diff --git a/CouponGuide.md b/CouponGuide.md new file mode 100644 index 0000000..621a3c5 --- /dev/null +++ b/CouponGuide.md @@ -0,0 +1,586 @@ +# ๐ŸŽŸ๏ธ Complete Coupon System Guide + +## ๐Ÿ“ฆ What's Included + +### Backend +1. **Admin Coupon Controller** - Full CRUD + Statistics +2. **User Coupon Controller** - Validation & Application +3. **Coupon Routes** - Both admin and user endpoints + +### Frontend +1. **CouponApply Component** - For checkout page +2. **Available Coupons Modal** - Browse & select coupons + +--- + +## ๐Ÿš€ Setup Instructions + +### Step 1: Backend Setup + +#### 1.1 Add Controllers + +Create two controller files: + +**File 1:** `src/controllers/admin/couponController.js` +(Use the `couponController_Admin.js` file) + +**File 2:** `src/controllers/couponController.js` +(Use the `couponController_User.js` file) + +#### 1.2 Add Routes + +Create: `src/routes/couponRoutes.js` +(Use the `couponRoutes.js` file) + +#### 1.3 Register Routes in App + +In your `server.js` or `app.js`: + +```javascript +const couponRoutes = require('./routes/couponRoutes'); + +app.use('/api/coupons', couponRoutes); +``` + +--- + +### Step 2: Frontend Setup + +#### 2.1 Create Component + +Create: `src/components/CouponApply.jsx` +(Use the `CouponApply.jsx` file) + +#### 2.2 Use in Checkout Page + +```javascript +// pages/Checkout.jsx + +import CouponApply from '../components/CouponApply'; +import { useState } from 'react'; + +const Checkout = () => { + const [appliedCoupon, setAppliedCoupon] = useState(null); + const [orderTotal, setOrderTotal] = useState(2599); + + const handleCouponApplied = (couponData) => { + setAppliedCoupon(couponData); + setOrderTotal(couponData.finalAmount); + }; + + const handleCouponRemoved = () => { + setAppliedCoupon(null); + setOrderTotal(2599); // Reset to original amount + }; + + return ( +
+ {/* Cart Items */} + + {/* Coupon Section */} + + + {/* Order Summary */} +
+
+ Subtotal: + โ‚น2,599 +
+ {appliedCoupon && ( +
+ Discount ({appliedCoupon.couponCode}): + -โ‚น{appliedCoupon.discountAmount} +
+ )} +
+ Total: + โ‚น{orderTotal.toLocaleString()} +
+
+
+ ); +}; +``` + +--- + +## ๐Ÿ“ก API Endpoints + +### Admin Endpoints + +#### 1. Get All Coupons +```http +GET /api/coupons/admin?page=1&limit=20&isActive=true&type=PERCENTAGE&search=SAVE +Authorization: Bearer +``` + +**Response:** +```json +{ + "status": true, + "message": "Coupons fetched successfully", + "data": { + "coupons": [ + { + "id": "...", + "code": "SAVE20", + "description": "20% off on all items", + "type": "PERCENTAGE", + "value": 20, + "minOrderAmount": 1000, + "maxUses": 100, + "usedCount": 25, + "usagePercentage": 25, + "isExpired": false, + "remainingUses": 75, + "isActive": true, + "validFrom": "2024-01-01", + "validUntil": "2024-12-31" + } + ], + "pagination": { + "total": 50, + "page": 1, + "limit": 20, + "pages": 3 + } + } +} +``` + +#### 2. Create Coupon +```http +POST /api/coupons/admin +Authorization: Bearer +Content-Type: application/json + +{ + "code": "SAVE20", + "description": "Get 20% off on orders above โ‚น1000", + "type": "PERCENTAGE", + "value": 20, + "minOrderAmount": 1000, + "maxUses": 100, + "validFrom": "2024-01-01T00:00:00Z", + "validUntil": "2024-12-31T23:59:59Z", + "isActive": true +} +``` + +**Coupon Types:** +- `PERCENTAGE` - Percentage discount (e.g., 20%) +- `FIXED_AMOUNT` - Fixed amount off (e.g., โ‚น500) +- `FREE_SHIPPING` - Free shipping + +#### 3. Update Coupon +```http +PUT /api/coupons/admin/:id +Authorization: Bearer + +{ + "value": 25, + "maxUses": 150 +} +``` + +#### 4. Toggle Coupon Status +```http +PATCH /api/coupons/admin/:id/toggle +Authorization: Bearer +``` + +#### 5. Get Coupon Statistics +```http +GET /api/coupons/admin/stats +Authorization: Bearer +``` + +**Response:** +```json +{ + "status": true, + "data": { + "totalCoupons": 50, + "activeCoupons": 35, + "expiredCoupons": 10, + "totalRedemptions": 2450, + "mostUsedCoupons": [...] + } +} +``` + +--- + +### User Endpoints + +#### 1. Get Available Coupons +```http +GET /api/coupons/available?orderAmount=2599 +``` + +**Response:** +```json +{ + "status": true, + "data": [ + { + "code": "SAVE20", + "description": "20% off on orders above โ‚น1000", + "type": "PERCENTAGE", + "value": 20, + "minOrderAmount": 1000, + "discountPreview": "20% OFF", + "remainingUses": 75, + "validUntil": "2024-12-31" + } + ] +} +``` + +#### 2. Validate Coupon +```http +POST /api/coupons/validate +Content-Type: application/json + +{ + "code": "SAVE20", + "orderAmount": 2599 +} +``` + +**Response:** +```json +{ + "status": true, + "message": "Coupon applied successfully", + "data": { + "couponCode": "SAVE20", + "couponType": "PERCENTAGE", + "discountAmount": 519.80, + "originalAmount": 2599, + "finalAmount": 2079.20, + "savings": 519.80, + "savingsPercentage": 20 + } +} +``` + +#### 3. Apply Coupon to Order +```http +POST /api/coupons/apply +Authorization: Bearer + +{ + "couponCode": "SAVE20", + "orderId": "order_123" +} +``` + +#### 4. Remove Coupon +```http +POST /api/coupons/remove +Authorization: Bearer + +{ + "orderId": "order_123" +} +``` + +--- + +## ๐Ÿ’ก Usage Examples + +### Example 1: Admin Creates Percentage Coupon +```javascript +const createCoupon = async () => { + const response = await axios.post( + 'http://localhost:3000/api/coupons/admin', + { + code: 'WELCOME10', + description: 'Get 10% off on your first order', + type: 'PERCENTAGE', + value: 10, + minOrderAmount: 500, + maxUses: 1000, + validFrom: new Date(), + validUntil: new Date('2024-12-31'), + isActive: true, + }, + { + headers: { + Authorization: `Bearer ${adminToken}`, + }, + } + ); +}; +``` + +### Example 2: Admin Creates Fixed Amount Coupon +```javascript +{ + code: 'FLAT500', + description: 'Flat โ‚น500 off on orders above โ‚น2000', + type: 'FIXED_AMOUNT', + value: 500, + minOrderAmount: 2000, + maxUses: 500, + validFrom: new Date(), + validUntil: new Date('2024-12-31'), +} +``` + +### Example 3: Admin Creates Free Shipping Coupon +```javascript +{ + code: 'FREESHIP', + description: 'Free shipping on all orders', + type: 'FREE_SHIPPING', + value: 0, + minOrderAmount: null, + maxUses: null, + validFrom: new Date(), + validUntil: new Date('2024-12-31'), +} +``` + +### Example 4: Customer Applies Coupon +```javascript +// In your checkout component +const [discount, setDiscount] = useState(0); + + { + setDiscount(data.discountAmount); + console.log('Saved:', data.savings); + }} + onCouponRemoved={() => { + setDiscount(0); + }} +/> +``` + +--- + +## ๐ŸŽจ Coupon Validation Rules + +### Automatic Validations + +1. **Active Status** - Must be active +2. **Date Range** - Must be within validFrom and validUntil +3. **Usage Limit** - Must have remaining uses +4. **Minimum Order** - Order amount must meet minimum requirement +5. **Code Uniqueness** - Code must be unique (enforced on creation) + +### Error Messages + +``` +โŒ "This coupon is no longer active" +โŒ "This coupon is not yet valid" +โŒ "This coupon has expired" +โŒ "This coupon has reached its usage limit" +โŒ "Minimum order amount of โ‚น1000 required" +โŒ "Invalid coupon code" +``` + +--- + +## ๐Ÿ“Š Coupon Types Explained + +### 1. PERCENTAGE Discount +``` +Order Amount: โ‚น2,599 +Coupon: 20% OFF +Discount: โ‚น519.80 +Final Amount: โ‚น2,079.20 +``` + +### 2. FIXED_AMOUNT Discount +``` +Order Amount: โ‚น2,599 +Coupon: โ‚น500 OFF +Discount: โ‚น500 +Final Amount: โ‚น2,099 +``` + +### 3. FREE_SHIPPING +``` +Order Amount: โ‚น2,599 +Shipping: โ‚น50 +Coupon: FREE SHIPPING +Discount: โ‚น50 +Final Amount: โ‚น2,549 +``` + +--- + +## ๐Ÿ”ง Advanced Features + +### Feature 1: User-Specific Coupons + +Add a `userId` field to restrict coupons: + +```prisma +model Coupon { + // ... existing fields + userId String? // Optional: restrict to specific user + user User? @relation(fields: [userId], references: [id]) +} +``` + +### Feature 2: Product-Specific Coupons + +```prisma +model Coupon { + // ... existing fields + applicableCategories String[] // Only for these categories + applicableProducts String[] // Only for these products +} +``` + +### Feature 3: First-Time User Coupons + +```javascript +// In validation logic +const userOrders = await prisma.order.count({ + where: { userId, status: 'DELIVERED' } +}); + +if (coupon.firstTimeOnly && userOrders > 0) { + return { isValid: false, message: 'Coupon only for first-time users' }; +} +``` + +--- + +## โœ… Testing Checklist + +### Backend Testing + +- [ ] Create percentage coupon +- [ ] Create fixed amount coupon +- [ ] Create free shipping coupon +- [ ] Validate active coupon +- [ ] Validate expired coupon +- [ ] Validate coupon below min order +- [ ] Validate coupon at usage limit +- [ ] Apply coupon to order +- [ ] Remove coupon from order +- [ ] Toggle coupon status +- [ ] Get coupon statistics + +### Frontend Testing + +- [ ] Apply valid coupon +- [ ] Apply invalid coupon +- [ ] View available coupons +- [ ] Remove applied coupon +- [ ] See discount in order summary +- [ ] Responsive design +- [ ] Loading states +- [ ] Error handling + +--- + +## ๐ŸŽฏ Common Use Cases + +### Use Case 1: Flash Sale +```javascript +{ + code: 'FLASH50', + type: 'PERCENTAGE', + value: 50, + validFrom: new Date('2024-12-25T00:00:00'), + validUntil: new Date('2024-12-25T23:59:59'), + maxUses: 100 +} +``` + +### Use Case 2: Minimum Purchase Promotion +```javascript +{ + code: 'BUY1000GET200', + type: 'FIXED_AMOUNT', + value: 200, + minOrderAmount: 1000, +} +``` + +### Use Case 3: New Customer Welcome +```javascript +{ + code: 'WELCOME', + type: 'PERCENTAGE', + value: 15, + description: 'Welcome! Get 15% off on your first order', + maxUses: null, // Unlimited +} +``` + +--- + +## ๐Ÿ“ˆ Analytics & Reporting + +Track coupon performance: + +```javascript +// Get most successful coupons +const topCoupons = await prisma.coupon.findMany({ + orderBy: { usedCount: 'desc' }, + take: 10, +}); + +// Calculate total savings given +const totalSavings = await calculateTotalDiscounts(); + +// Conversion rate +const conversionRate = (usedCount / impressions) * 100; +``` + +--- + +**Your coupon system is ready! ๐ŸŽ‰** + +Full features: +โœ… Admin CRUD operations +โœ… Smart validation +โœ… Multiple coupon types +โœ… Usage tracking +โœ… Statistics & analytics +โœ… User-friendly UI +โœ… Mobile responsive + + + + +1. User applies coupon "WELCOME10" + โ†“ +2. CouponApply validates coupon + โ†“ +3. Coupon data stored in state + { + couponCode: "WELCOME10", + discountAmount: 259.90, + ... + } + โ†“ +4. User clicks "Place Order" + โ†“ +5. Frontend sends: + { + items: [...], + couponCode: "WELCOME10" โœ… + } + โ†“ +6. Backend validates coupon again + โ†“ +7. Creates order in transaction: + - Creates order + - Increments coupon.usedCount โœ… + โ†“ +8. Admin panel shows: + "usedCount": 1 โœ… \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..48c56c6 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,14 @@ +FROM node:20-bullseye-slim + +WORKDIR /app + +COPY package*.json ./ +RUN npm install + +COPY . . + +RUN npx prisma generate + +EXPOSE 3000 + +CMD ["node", "src/server.js"] diff --git a/Inventory.md b/Inventory.md new file mode 100644 index 0000000..c526350 --- /dev/null +++ b/Inventory.md @@ -0,0 +1,332 @@ +# ๐Ÿ“ฆ Complete Inventory Management System - Implementation Guide + +## What You Get + +โœ… **Auto Stock Reduction** - Stock reduces when order delivered +โœ… **Low Stock Alerts** - Dashboard shows products needing restock +โœ… **Real Top Sellers** - Based on actual sales, not fake data +โœ… **Inventory Logs** - Track every stock change +โœ… **Stock Status** - OUT_OF_STOCK, CRITICAL, LOW, IN_STOCK + +--- + +## ๐Ÿš€ Setup (5 Steps) + +### Step 1: Update Prisma Schema + +Add this to your `schema.prisma`: + +```prisma +model InventoryLog { + id String @id @default(cuid()) + + productId String + productName String + + type InventoryLogType + quantityChange Int + previousStock Int + newStock Int + + orderId String? + adjustedBy String? + notes String? + + createdAt DateTime @default(now()) + + @@index([productId]) + @@index([orderId]) + @@index([type]) + @@map("inventory_logs") +} + +enum InventoryLogType { + SOLD + RESTOCK + ADJUSTMENT + RETURN + DAMAGED +} +``` + +### Step 2: Run Migration + +```bash +npx prisma migrate dev --name add_inventory_logs +``` + +### Step 3: Add Inventory Service + +Create `src/services/inventoryService.js` with the provided code. + +### Step 4: Update Order Controller + +Replace your order controller with the version that includes auto stock reduction. + +### Step 5: Update Dashboard Controller + +Replace dashboard controller to show real top sellers + low stock alerts. + +--- + +## ๐Ÿ“Š Dashboard API Response + +```json +{ + "data": { + "totalUsers": 150, + "totalOrders": 450, + "totalProducts": 89, + "totalRevenue": 125000, + + "inventory": { + "totalProducts": 89, + "outOfStock": 5, + "criticalStock": 8, + "lowStock": 12, + "inStock": 64 + }, + + "topProducts": [ + { + "_id": "prod123", + "name": "Rare Rabbit", + "basePrice": 2500, + "totalSold": 45, + "totalOrders": 32, + "revenue": 112500, + "stock": 12, + "stockStatus": "LOW", + "displayImage": "https://..." + } + ], + + "lowStockProducts": [ + { + "_id": "prod456", + "name": "Product Name", + "stock": 3, + "status": "CRITICAL", + "basePrice": 1500, + "displayImage": "https://..." + } + ] + } +} +``` + +--- + +## โš™๏ธ How Auto Stock Reduction Works + +### When Admin Updates Order to DELIVERED: + +``` +1. Admin: PUT /api/admin/orders/:id/status + Body: { "status": "DELIVERED" } + +2. Order Controller: + โœ… Updates order status + โœ… Creates status history + โœ… Calls reduceStockOnDelivery() + +3. Inventory Service: + โœ… Gets order items + โœ… For each item: + - Get product from MongoDB + - Calculate: newStock = currentStock - quantity + - Update product.stock in MongoDB + - Create inventory log in PostgreSQL + +4. Response: + { + "success": true, + "message": "Order status updated", + "stockReduction": [ + { + "productName": "Rare Rabbit", + "reduced": 1, + "previousStock": 15, + "newStock": 14 + } + ] + } +``` + +--- + +## ๐Ÿ“‹ Inventory Log Example + +Every stock change creates a log: + +```json +{ + "id": "log123", + "productId": "prod456", + "productName": "Rare Rabbit", + "type": "SOLD", + "quantityChange": -1, + "previousStock": 15, + "newStock": 14, + "orderId": "order789", + "notes": "Order ORD1234567 delivered", + "createdAt": "2026-02-11T14:30:00.000Z" +} +``` + +--- + +## ๐ŸŽฏ Stock Status Logic + +```javascript +stock === 0 โ†’ OUT_OF_STOCK (Red) +stock <= 5 โ†’ CRITICAL (Orange) +stock <= 10 โ†’ LOW (Yellow) +stock > 10 โ†’ IN_STOCK (Green) +``` + +--- + +## ๐Ÿ“Š Top Selling Products Logic + +### OLD (Wrong): +```javascript +// โŒ Used purchaseCount from MongoDB (never updated) +Product.find().sort({ purchaseCount: -1 }) +``` + +### NEW (Correct): +```javascript +// โœ… Query actual order data from PostgreSQL +SELECT productId, SUM(quantity), COUNT(*) +FROM order_items +GROUP BY productId +ORDER BY SUM(quantity) DESC + +// โœ… Join with MongoDB for product details +// โœ… Add stock info +// โœ… Calculate revenue +``` + +--- + +## ๐ŸŽจ Frontend Display + +### Dashboard - Top Sellers + +```javascript +{topProducts.map(product => ( +
+ +
+

{product.name}

+
+ Sold: {product.totalSold} + Revenue: โ‚น{product.revenue.toLocaleString()} + + {product.stock} in stock + +
+
+
+))} +``` + +### Dashboard - Low Stock Alerts + +```javascript +{lowStockProducts.length > 0 && ( +
+

+ โš ๏ธ Low Stock Alert ({lowStockProducts.length}) +

+
+ {lowStockProducts.map(product => ( +
+
+ +
+

{product.name}

+

+ {product.stock === 0 ? 'Out of Stock' : `Only ${product.stock} left`} +

+
+
+ +
+ ))} +
+
+)} +``` + +--- + +## ๐Ÿ”ง Manual Stock Adjustment (Future) + +```javascript +// POST /api/admin/inventory/adjust +{ + "productId": "prod123", + "quantity": 50, + "type": "ADD", // ADD, REMOVE, SET + "notes": "Received new shipment" +} +``` + +--- + +## ๐Ÿ“ˆ Benefits + +1. **Accurate Inventory** - Always know real stock levels +2. **Prevent Overselling** - Stock reduces on delivery +3. **Early Warnings** - Low stock alerts +4. **Better Planning** - See what's selling +5. **Full Audit Trail** - Every change logged +6. **Automated** - No manual work needed + +--- + +## โœ… Testing Checklist + +- [ ] Prisma migration ran successfully +- [ ] Place test order +- [ ] Mark order as DELIVERED +- [ ] Check product stock reduced +- [ ] Check inventory log created +- [ ] Dashboard shows correct stock +- [ ] Low stock products appear +- [ ] Top sellers show real data + +--- + +## ๐ŸŽฏ What Happens + +### Before (Old System): +``` +Order Delivered โ†’ Nothing happens +Stock: Always same (never changes) +Top Products: Fake/old data +Dashboard: No inventory info +``` + +### After (New System): +``` +Order Delivered โ†’ Auto stock reduction +Stock: Real-time accurate +Top Products: Based on actual sales +Dashboard: Full inventory overview +Alerts: Low stock warnings +Logs: Complete audit trail +``` + +--- + +**Your inventory system is now production-ready!** ๐Ÿ“ฆโœจ \ No newline at end of file diff --git a/LOCAL_SETUP.md b/LOCAL_SETUP.md new file mode 100644 index 0000000..3c2c938 --- /dev/null +++ b/LOCAL_SETUP.md @@ -0,0 +1,183 @@ +# Local Development Setup Guide + +This guide will help you set up the Vaishnavi Creation backend for local development without Docker. + +## Prerequisites + +Make sure you have the following installed and running: + +1. **Node.js 18+** โœ… (You have v22.14.0) +2. **PostgreSQL 15+** - Make sure it's running +3. **MongoDB 7+** - Make sure it's running +4. **Redis 7+** (Optional) - For caching and background jobs + +## Step 1: Configure Environment Variables + +1. Copy the environment template: + ```bash + cp .env.example .env + ``` + +2. Edit the `.env` file with your database credentials: + + **For PostgreSQL**, update the DATABASE_URL: + ```env + # Replace with your actual PostgreSQL credentials + DATABASE_URL="postgresql://username:password@localhost:5432/vaishnavi_db?schema=public" + ``` + + Common PostgreSQL configurations: + - **Default user**: `postgres` + - **Default password**: `postgres` (or whatever you set) + - **Default port**: `5432` + - **Database name**: `vaishnavi_db` (we'll create this) + + **For MongoDB**, update the MONGODB_URI: + ```env + # Usually this is fine for local MongoDB + MONGODB_URI="mongodb://localhost:27017/vaishnavi_products" + ``` + + **For Redis** (Optional): + ```env + # Comment this out if you don't have Redis installed + REDIS_URL="redis://localhost:6379" + ``` + +## Step 2: Create PostgreSQL Database + +Connect to PostgreSQL and create the database: + +```sql +-- Connect to PostgreSQL (replace with your credentials) +psql -U postgres + +-- Create the database +CREATE DATABASE vaishnavi_db; + +-- Exit psql +\q +``` + +**Windows Users**: If you have PostgreSQL installed via installer, you might need to use pgAdmin or the command prompt with full path. + +## Step 3: Run the Setup Script + +```bash +npm run setup +``` + +This script will: +- โœ… Check Node.js version +- โœ… Install dependencies +- โœ… Generate Prisma client +- โœ… Create database schema +- โœ… Seed the database with sample data + +## Step 4: Start Development Server + +```bash +npm run dev +``` + +The API will be available at `http://localhost:3000` + +## Troubleshooting + +### PostgreSQL Connection Issues + +1. **Make sure PostgreSQL is running**: + - **Windows**: Check Services or start PostgreSQL service + - **Linux/Mac**: `sudo systemctl start postgresql` or `brew services start postgresql` + +2. **Check your connection string**: + ```env + # Format: postgresql://username:password@host:port/database + DATABASE_URL="postgresql://postgres:yourpassword@localhost:5432/vaishnavi_db?schema=public" + ``` + +3. **Test connection manually**: + ```bash + # Test if PostgreSQL is accessible + psql -U postgres -h localhost -p 5432 -c "SELECT version();" + ``` + +### MongoDB Connection Issues + +1. **Make sure MongoDB is running**: + - **Windows**: Check Services or start MongoDB service + - **Linux**: `sudo systemctl start mongod` + - **Mac**: `brew services start mongodb-community` + +2. **Test MongoDB connection**: + ```bash + # Connect to MongoDB + mongosh + # or + mongo + ``` + +### Redis Connection Issues (Optional) + +If you don't have Redis installed or don't want to use it: + +1. Comment out the Redis URL in your `.env` file: + ```env + # REDIS_URL="redis://localhost:6379" + ``` + +2. The application will work without Redis, but caching and background jobs won't be available. + +## Manual Setup (Alternative) + +If the setup script doesn't work, you can run the commands manually: + +```bash +# Install dependencies +npm install + +# Generate Prisma client +npx prisma generate + +# Create database schema +npx prisma db push + +# Seed database +npm run db:seed + +# Start development server +npm run dev +``` + +## Verification + +Once everything is set up, you can verify by: + +1. **Health Check**: Visit `http://localhost:3000/health` +2. **API Root**: Visit `http://localhost:3000/` +3. **Database Studio**: Run `npm run db:studio` to view your data + +## Default Users + +After seeding, you'll have these test users: + +- **Admin**: `admin@vaishnavi.com` / `admin123` +- **Customer**: `customer@example.com` / `customer123` + +## Next Steps + +1. Test the API endpoints using Postman or curl +2. Set up your frontend application +3. Configure AWS S3 for file uploads (optional) +4. Set up email services for notifications (optional) + +## Getting Help + +If you encounter issues: + +1. Check the logs in your terminal +2. Verify all services are running +3. Double-check your `.env` configuration +4. Make sure you have the correct database permissions + +Happy coding! ๐Ÿš€ diff --git a/README.md b/README.md new file mode 100644 index 0000000..651b81e --- /dev/null +++ b/README.md @@ -0,0 +1,289 @@ +# Vaishnavi Creation - E-commerce Backend API + +A comprehensive e-commerce backend API built with Node.js, Express, PostgreSQL, and MongoDB, designed for fashion and wardrobe management with AI-powered features. + +## ๐Ÿš€ Tech Stack + +- **Backend**: Node.js 18+, Express.js +- **Databases**: + - PostgreSQL (transactional data) with Prisma ORM + - MongoDB (flexible product/wardrobe documents) with Mongoose +- **Caching & Jobs**: Redis with BullMQ +- **File Storage**: AWS S3 +- **Authentication**: JWT with bcrypt +- **Containerization**: Docker +- **CI/CD**: GitHub Actions + +## ๐Ÿ“ Project Structure + +``` +vaishnavi-backend/ +โ”œโ”€โ”€ src/ +โ”‚ โ”œโ”€โ”€ config/ # Database and app configuration +โ”‚ โ”œโ”€โ”€ controllers/ # Route controllers +โ”‚ โ”œโ”€โ”€ middleware/ # Custom middleware (auth, error handling) +โ”‚ โ”œโ”€โ”€ models/ # Database models +โ”‚ โ”‚ โ””โ”€โ”€ mongodb/ # MongoDB models (Product, Wardrobe) +โ”‚ โ”œโ”€โ”€ routes/ # API routes +โ”‚ โ”œโ”€โ”€ services/ # Business logic services +โ”‚ โ”œโ”€โ”€ jobs/ # Background job processors +โ”‚ โ”œโ”€โ”€ utils/ # Utility functions +โ”‚ โ”œโ”€โ”€ types/ # TypeScript type definitions +โ”‚ โ””โ”€โ”€ server.js # Application entry point +โ”œโ”€โ”€ prisma/ # Prisma schema and migrations +โ”œโ”€โ”€ tests/ # Test files +โ””โ”€โ”€ package.json +``` + +## ๐Ÿ› ๏ธ Setup & Installation + +### Prerequisites + +- Node.js 18+ +- PostgreSQL 15+ (running locally) +- MongoDB 7+ (running locally) +- Redis 7+ (optional, for caching and job queues) + +### 1. Clone and Install Dependencies + +```bash +git clone +cd vaishnavi-backend +npm install +``` + +### 2. Environment Configuration + +Copy the environment template and configure your variables: + +```bash +cp .env.example .env +``` + +Edit `.env` with your local database configuration: + +```env +# Database URLs - Update with your local credentials +DATABASE_URL="postgresql://postgres:yourpassword@localhost:5432/vaishnavi_db" +MONGODB_URI="mongodb://localhost:27017/vaishnavi_products" +REDIS_URL="redis://localhost:6379" + +# JWT Secrets - Change these in production! +JWT_SECRET="your-super-secret-jwt-key" +JWT_REFRESH_SECRET="your-super-secret-refresh-key" + +# AWS S3 Configuration (Optional) +AWS_ACCESS_KEY_ID="your-aws-access-key" +AWS_SECRET_ACCESS_KEY="your-aws-secret-key" +AWS_S3_BUCKET="vaishnavi-files" +``` + +### 3. Database Setup + +#### PostgreSQL Setup + +1. **Create the database**: +```sql +-- Connect to PostgreSQL and create the database +CREATE DATABASE vaishnavi_db; +``` + +2. **Generate Prisma client and run migrations**: +```bash +# Generate Prisma client +npm run db:generate + +# Run migrations to create tables +npm run db:migrate + +# Seed database with sample data +npm run db:seed +``` + +#### MongoDB Setup + +MongoDB will automatically create collections when first accessed. Make sure your MongoDB service is running on the default port (27017). + +#### Redis Setup (Optional) + +If you have Redis installed locally, make sure it's running on port 6379. If not, you can comment out the Redis URL in your `.env` file. + +### 4. Start Development Server + +```bash +npm run dev +``` + +The API will be available at `http://localhost:3000` + +## ๐Ÿ”ง Troubleshooting + +### Database Connection Issues + +1. **PostgreSQL Connection Error**: + - Ensure PostgreSQL is running: `sudo systemctl start postgresql` (Linux) or start PostgreSQL service (Windows) + - Check your connection string in `.env` + - Verify the database exists: `psql -U postgres -c "CREATE DATABASE vaishnavi_db;"` + +2. **MongoDB Connection Error**: + - Ensure MongoDB is running: `sudo systemctl start mongod` (Linux) or start MongoDB service (Windows) + - Check if MongoDB is listening on port 27017: `netstat -an | grep 27017` + +3. **Redis Connection Error** (Optional): + - If you don't have Redis installed, you can comment out the REDIS_URL in your `.env` file + - The application will work without Redis, but caching and background jobs won't be available + +## ๐Ÿ“š API Endpoints + +### Authentication +- `POST /api/auth/register` - User registration +- `POST /api/auth/login` - User login +- `POST /api/auth/refresh` - Refresh JWT token +- `POST /api/auth/logout` - User logout +- `GET /api/auth/me` - Get current user profile + +### Users +- `GET /api/users/profile` - Get user profile +- `PUT /api/users/profile` - Update user profile +- `GET /api/users/addresses` - Get user addresses +- `POST /api/users/addresses` - Add address +- `GET /api/users/orders` - Get user orders +- `GET /api/users/wishlist` - Get user wishlist + +### Products +- `GET /api/products` - Get all products (with filters) +- `GET /api/products/:slug` - Get single product +- `POST /api/products` - Create product (Admin) +- `PUT /api/products/:id` - Update product (Admin) +- `DELETE /api/products/:id` - Delete product (Admin) + +### Orders +- `POST /api/orders` - Create new order +- `GET /api/orders` - Get user orders +- `GET /api/orders/:id` - Get single order +- `PUT /api/orders/:id/cancel` - Cancel order + +### Wardrobe +- `GET /api/wardrobe` - Get user's wardrobe +- `POST /api/wardrobe/items` - Add item to wardrobe +- `PUT /api/wardrobe/items/:id` - Update wardrobe item +- `DELETE /api/wardrobe/items/:id` - Remove wardrobe item +- `GET /api/wardrobe/recommendations` - Get outfit recommendations + +### Admin +- `GET /api/admin/dashboard` - Dashboard statistics +- `GET /api/admin/users` - Manage users +- `GET /api/admin/orders` - Manage orders +- `GET /api/admin/products` - Manage products +- `GET /api/admin/categories` - Manage categories +- `GET /api/admin/coupons` - Manage coupons + +## ๐Ÿ” Authentication + +The API uses JWT-based authentication. Include the token in the Authorization header: + +``` +Authorization: Bearer +``` + +### User Roles + +- `CUSTOMER` - Default role for registered users +- `ADMIN` - Full access to all endpoints +- `MODERATOR` - Limited admin access +- `SELLER` - Can manage their own products + +## ๐Ÿ—„๏ธ Database Schema + +### PostgreSQL (Prisma) +- Users & Authentication +- Orders & Transactions +- Addresses +- Reviews & Ratings +- Wishlist & Cart +- Categories & Coupons +- System Configuration + +### MongoDB (Mongoose) +- Products (flexible schema for variants, images, AI tags) +- Wardrobe (user clothing collections with AI analysis) + +## ๐Ÿš€ Deployment + +### Environment Variables for Production + +Ensure all production environment variables are set: +- Database URLs (production databases) +- JWT secrets (strong, unique values) +- AWS credentials +- Email/SMS service credentials + +### Deployment Options + +You can deploy this application to various platforms: +- **Heroku**: Use the Procfile and configure environment variables +- **AWS EC2**: Set up Node.js environment and configure databases +- **DigitalOcean App Platform**: Connect your repository and configure environment variables +- **Railway**: Connect your GitHub repository for automatic deployments +- **Vercel**: For serverless deployment (with some modifications) + +## ๐Ÿงช Testing + +```bash +# Run tests +npm test + +# Run tests in watch mode +npm run test:watch + +# Generate coverage report +npm run test:coverage +``` + +## ๐Ÿ“ Scripts + +- `npm start` - Start production server +- `npm run dev` - Start development server with nodemon +- `npm run build` - Generate Prisma client +- `npm run lint` - Run ESLint +- `npm run format` - Format code with Prettier +- `npm run db:studio` - Open Prisma Studio +- `npm run db:migrate` - Run database migrations + +## ๐Ÿ”„ Background Jobs + +The application uses BullMQ with Redis for processing: +- AI image tagging for wardrobe items +- Email notifications +- Image processing and optimization +- Recommendation engine updates + +## ๐Ÿ“Š Monitoring & Logging + +- Health check endpoint: `GET /health` +- Structured logging with Morgan +- Error tracking and reporting +- Database connection monitoring + +## ๐Ÿค Contributing + +1. Fork the repository +2. Create a feature branch: `git checkout -b feature/amazing-feature` +3. Commit changes: `git commit -m 'Add amazing feature'` +4. Push to branch: `git push origin feature/amazing-feature` +5. Open a Pull Request + +## ๐Ÿ“„ License + +This project is licensed under the ISC License. + +## ๐Ÿ†˜ Support + +For support and questions: +- Create an issue in the repository +- Check the API documentation at `/api/docs` +- Review the health check at `/health` + +--- + +**Built with โค๏ธ for Vaishnavi Creation** diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..6e698b1 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,9431 @@ +{ + "name": "vaishnavi-backend", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "vaishnavi-backend", + "version": "1.0.0", + "license": "ISC", + "dependencies": { + "@prisma/client": "^5.22.0", + "aws-sdk": "^2.1693.0", + "axios": "^1.13.5", + "bcrypt": "^5.1.1", + "bullmq": "^5.25.0", + "cookie-parser": "^1.4.7", + "cors": "^2.8.5", + "dotenv": "^16.4.7", + "ejs": "^3.1.10", + "express": "^4.21.2", + "helmet": "^8.0.0", + "ioredis": "^5.4.1", + "jsonwebtoken": "^9.0.2", + "mongoose": "^8.9.2", + "morgan": "^1.10.0", + "multer": "^1.4.5-lts.1", + "multer-s3": "^3.0.1", + "nodemailer": "^7.0.11", + "prisma": "^5.22.0" + }, + "devDependencies": { + "eslint": "^9.17.0", + "jest": "^29.7.0", + "nodemon": "^3.1.9", + "prettier": "^3.4.2", + "supertest": "^7.0.0" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + } + }, + "node_modules/@aws-crypto/crc32": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/crc32/-/crc32-5.2.0.tgz", + "integrity": "sha512-nLbCWqQNgUiwwtFsen1AdzAtvuLRsQS8rYgMuxCrdKf9kOssamGLuPwyTY9wyYblNr9+1XM8v6zoDTPPSIeANg==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@aws-crypto/util": "^5.2.0", + "@aws-sdk/types": "^3.222.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-crypto/crc32c": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/crc32c/-/crc32c-5.2.0.tgz", + "integrity": "sha512-+iWb8qaHLYKrNvGRbiYRHSdKRWhto5XlZUEBwDjYNf+ly5SVYG6zEoYIdxvf5R3zyeP16w4PLBn3rH1xc74Rag==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@aws-crypto/util": "^5.2.0", + "@aws-sdk/types": "^3.222.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-crypto/sha1-browser": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/sha1-browser/-/sha1-browser-5.2.0.tgz", + "integrity": "sha512-OH6lveCFfcDjX4dbAvCFSYUjJZjDr/3XJ3xHtjn3Oj5b9RjojQo8npoLeA/bNwkOkrSQ0wgrHzXk4tDRxGKJeg==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@aws-crypto/supports-web-crypto": "^5.2.0", + "@aws-crypto/util": "^5.2.0", + "@aws-sdk/types": "^3.222.0", + "@aws-sdk/util-locate-window": "^3.0.0", + "@smithy/util-utf8": "^2.0.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-crypto/sha1-browser/node_modules/@smithy/is-array-buffer": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-2.2.0.tgz", + "integrity": "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-crypto/sha1-browser/node_modules/@smithy/util-buffer-from": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-2.2.0.tgz", + "integrity": "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@smithy/is-array-buffer": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-crypto/sha1-browser/node_modules/@smithy/util-utf8": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-2.3.0.tgz", + "integrity": "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@smithy/util-buffer-from": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-crypto/sha256-browser": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-browser/-/sha256-browser-5.2.0.tgz", + "integrity": "sha512-AXfN/lGotSQwu6HNcEsIASo7kWXZ5HYWvfOmSNKDsEqC4OashTp8alTmaz+F7TC2L083SFv5RdB+qU3Vs1kZqw==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@aws-crypto/sha256-js": "^5.2.0", + "@aws-crypto/supports-web-crypto": "^5.2.0", + "@aws-crypto/util": "^5.2.0", + "@aws-sdk/types": "^3.222.0", + "@aws-sdk/util-locate-window": "^3.0.0", + "@smithy/util-utf8": "^2.0.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-crypto/sha256-browser/node_modules/@smithy/is-array-buffer": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-2.2.0.tgz", + "integrity": "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-crypto/sha256-browser/node_modules/@smithy/util-buffer-from": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-2.2.0.tgz", + "integrity": "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@smithy/is-array-buffer": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-crypto/sha256-browser/node_modules/@smithy/util-utf8": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-2.3.0.tgz", + "integrity": "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@smithy/util-buffer-from": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-crypto/sha256-js": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-js/-/sha256-js-5.2.0.tgz", + "integrity": "sha512-FFQQyu7edu4ufvIZ+OadFpHHOt+eSTBaYaki44c+akjg7qZg9oOQeLlk77F6tSYqjDAFClrHJk9tMf0HdVyOvA==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@aws-crypto/util": "^5.2.0", + "@aws-sdk/types": "^3.222.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-crypto/supports-web-crypto": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/supports-web-crypto/-/supports-web-crypto-5.2.0.tgz", + "integrity": "sha512-iAvUotm021kM33eCdNfwIN//F77/IADDSs58i+MDaOqFrVjZo9bAal0NK7HurRuWLLpF1iLX7gbWrjHjeo+YFg==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-crypto/util": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/util/-/util-5.2.0.tgz", + "integrity": "sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@aws-sdk/types": "^3.222.0", + "@smithy/util-utf8": "^2.0.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-crypto/util/node_modules/@smithy/is-array-buffer": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-2.2.0.tgz", + "integrity": "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-crypto/util/node_modules/@smithy/util-buffer-from": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-2.2.0.tgz", + "integrity": "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@smithy/is-array-buffer": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-crypto/util/node_modules/@smithy/util-utf8": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-2.3.0.tgz", + "integrity": "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@smithy/util-buffer-from": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/client-s3": { + "version": "3.958.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-s3/-/client-s3-3.958.0.tgz", + "integrity": "sha512-ol8Sw37AToBWb6PjRuT/Wu40SrrZSA0N4F7U3yTkjUNX0lirfO1VFLZ0hZtZplVJv8GNPITbiczxQ8VjxESXxg==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@aws-crypto/sha1-browser": "5.2.0", + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "3.957.0", + "@aws-sdk/credential-provider-node": "3.958.0", + "@aws-sdk/middleware-bucket-endpoint": "3.957.0", + "@aws-sdk/middleware-expect-continue": "3.957.0", + "@aws-sdk/middleware-flexible-checksums": "3.957.0", + "@aws-sdk/middleware-host-header": "3.957.0", + "@aws-sdk/middleware-location-constraint": "3.957.0", + "@aws-sdk/middleware-logger": "3.957.0", + "@aws-sdk/middleware-recursion-detection": "3.957.0", + "@aws-sdk/middleware-sdk-s3": "3.957.0", + "@aws-sdk/middleware-ssec": "3.957.0", + "@aws-sdk/middleware-user-agent": "3.957.0", + "@aws-sdk/region-config-resolver": "3.957.0", + "@aws-sdk/signature-v4-multi-region": "3.957.0", + "@aws-sdk/types": "3.957.0", + "@aws-sdk/util-endpoints": "3.957.0", + "@aws-sdk/util-user-agent-browser": "3.957.0", + "@aws-sdk/util-user-agent-node": "3.957.0", + "@smithy/config-resolver": "^4.4.5", + "@smithy/core": "^3.20.0", + "@smithy/eventstream-serde-browser": "^4.2.7", + "@smithy/eventstream-serde-config-resolver": "^4.3.7", + "@smithy/eventstream-serde-node": "^4.2.7", + "@smithy/fetch-http-handler": "^5.3.8", + "@smithy/hash-blob-browser": "^4.2.8", + "@smithy/hash-node": "^4.2.7", + "@smithy/hash-stream-node": "^4.2.7", + "@smithy/invalid-dependency": "^4.2.7", + "@smithy/md5-js": "^4.2.7", + "@smithy/middleware-content-length": "^4.2.7", + "@smithy/middleware-endpoint": "^4.4.1", + "@smithy/middleware-retry": "^4.4.17", + "@smithy/middleware-serde": "^4.2.8", + "@smithy/middleware-stack": "^4.2.7", + "@smithy/node-config-provider": "^4.3.7", + "@smithy/node-http-handler": "^4.4.7", + "@smithy/protocol-http": "^5.3.7", + "@smithy/smithy-client": "^4.10.2", + "@smithy/types": "^4.11.0", + "@smithy/url-parser": "^4.2.7", + "@smithy/util-base64": "^4.3.0", + "@smithy/util-body-length-browser": "^4.2.0", + "@smithy/util-body-length-node": "^4.2.1", + "@smithy/util-defaults-mode-browser": "^4.3.16", + "@smithy/util-defaults-mode-node": "^4.2.19", + "@smithy/util-endpoints": "^3.2.7", + "@smithy/util-middleware": "^4.2.7", + "@smithy/util-retry": "^4.2.7", + "@smithy/util-stream": "^4.5.8", + "@smithy/util-utf8": "^4.2.0", + "@smithy/util-waiter": "^4.2.7", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-sso": { + "version": "3.958.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso/-/client-sso-3.958.0.tgz", + "integrity": "sha512-6qNCIeaMzKzfqasy2nNRuYnMuaMebCcCPP4J2CVGkA8QYMbIVKPlkn9bpB20Vxe6H/r3jtCCLQaOJjVTx/6dXg==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "3.957.0", + "@aws-sdk/middleware-host-header": "3.957.0", + "@aws-sdk/middleware-logger": "3.957.0", + "@aws-sdk/middleware-recursion-detection": "3.957.0", + "@aws-sdk/middleware-user-agent": "3.957.0", + "@aws-sdk/region-config-resolver": "3.957.0", + "@aws-sdk/types": "3.957.0", + "@aws-sdk/util-endpoints": "3.957.0", + "@aws-sdk/util-user-agent-browser": "3.957.0", + "@aws-sdk/util-user-agent-node": "3.957.0", + "@smithy/config-resolver": "^4.4.5", + "@smithy/core": "^3.20.0", + "@smithy/fetch-http-handler": "^5.3.8", + "@smithy/hash-node": "^4.2.7", + "@smithy/invalid-dependency": "^4.2.7", + "@smithy/middleware-content-length": "^4.2.7", + "@smithy/middleware-endpoint": "^4.4.1", + "@smithy/middleware-retry": "^4.4.17", + "@smithy/middleware-serde": "^4.2.8", + "@smithy/middleware-stack": "^4.2.7", + "@smithy/node-config-provider": "^4.3.7", + "@smithy/node-http-handler": "^4.4.7", + "@smithy/protocol-http": "^5.3.7", + "@smithy/smithy-client": "^4.10.2", + "@smithy/types": "^4.11.0", + "@smithy/url-parser": "^4.2.7", + "@smithy/util-base64": "^4.3.0", + "@smithy/util-body-length-browser": "^4.2.0", + "@smithy/util-body-length-node": "^4.2.1", + "@smithy/util-defaults-mode-browser": "^4.3.16", + "@smithy/util-defaults-mode-node": "^4.2.19", + "@smithy/util-endpoints": "^3.2.7", + "@smithy/util-middleware": "^4.2.7", + "@smithy/util-retry": "^4.2.7", + "@smithy/util-utf8": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/core": { + "version": "3.957.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.957.0.tgz", + "integrity": "sha512-DrZgDnF1lQZv75a52nFWs6MExihJF2GZB6ETZRqr6jMwhrk2kbJPUtvgbifwcL7AYmVqHQDJBrR/MqkwwFCpiw==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@aws-sdk/types": "3.957.0", + "@aws-sdk/xml-builder": "3.957.0", + "@smithy/core": "^3.20.0", + "@smithy/node-config-provider": "^4.3.7", + "@smithy/property-provider": "^4.2.7", + "@smithy/protocol-http": "^5.3.7", + "@smithy/signature-v4": "^5.3.7", + "@smithy/smithy-client": "^4.10.2", + "@smithy/types": "^4.11.0", + "@smithy/util-base64": "^4.3.0", + "@smithy/util-middleware": "^4.2.7", + "@smithy/util-utf8": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/crc64-nvme": { + "version": "3.957.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/crc64-nvme/-/crc64-nvme-3.957.0.tgz", + "integrity": "sha512-qSwSfI+qBU9HDsd6/4fM9faCxYJx2yDuHtj+NVOQ6XYDWQzFab/hUdwuKZ77Pi6goLF1pBZhJ2azaC2w7LbnTA==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@smithy/types": "^4.11.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-env": { + "version": "3.957.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.957.0.tgz", + "integrity": "sha512-475mkhGaWCr+Z52fOOVb/q2VHuNvqEDixlYIkeaO6xJ6t9qR0wpLt4hOQaR6zR1wfZV0SlE7d8RErdYq/PByog==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@aws-sdk/core": "3.957.0", + "@aws-sdk/types": "3.957.0", + "@smithy/property-provider": "^4.2.7", + "@smithy/types": "^4.11.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-http": { + "version": "3.957.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.957.0.tgz", + "integrity": "sha512-8dS55QHRxXgJlHkEYaCGZIhieCs9NU1HU1BcqQ4RfUdSsfRdxxktqUKgCnBnOOn0oD3PPA8cQOCAVgIyRb3Rfw==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@aws-sdk/core": "3.957.0", + "@aws-sdk/types": "3.957.0", + "@smithy/fetch-http-handler": "^5.3.8", + "@smithy/node-http-handler": "^4.4.7", + "@smithy/property-provider": "^4.2.7", + "@smithy/protocol-http": "^5.3.7", + "@smithy/smithy-client": "^4.10.2", + "@smithy/types": "^4.11.0", + "@smithy/util-stream": "^4.5.8", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-ini": { + "version": "3.958.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.958.0.tgz", + "integrity": "sha512-u7twvZa1/6GWmPBZs6DbjlegCoNzNjBsMS/6fvh5quByYrcJr/uLd8YEr7S3UIq4kR/gSnHqcae7y2nL2bqZdg==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@aws-sdk/core": "3.957.0", + "@aws-sdk/credential-provider-env": "3.957.0", + "@aws-sdk/credential-provider-http": "3.957.0", + "@aws-sdk/credential-provider-login": "3.958.0", + "@aws-sdk/credential-provider-process": "3.957.0", + "@aws-sdk/credential-provider-sso": "3.958.0", + "@aws-sdk/credential-provider-web-identity": "3.958.0", + "@aws-sdk/nested-clients": "3.958.0", + "@aws-sdk/types": "3.957.0", + "@smithy/credential-provider-imds": "^4.2.7", + "@smithy/property-provider": "^4.2.7", + "@smithy/shared-ini-file-loader": "^4.4.2", + "@smithy/types": "^4.11.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-login": { + "version": "3.958.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-login/-/credential-provider-login-3.958.0.tgz", + "integrity": "sha512-sDwtDnBSszUIbzbOORGh5gmXGl9aK25+BHb4gb1aVlqB+nNL2+IUEJA62+CE55lXSH8qXF90paivjK8tOHTwPA==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@aws-sdk/core": "3.957.0", + "@aws-sdk/nested-clients": "3.958.0", + "@aws-sdk/types": "3.957.0", + "@smithy/property-provider": "^4.2.7", + "@smithy/protocol-http": "^5.3.7", + "@smithy/shared-ini-file-loader": "^4.4.2", + "@smithy/types": "^4.11.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-node": { + "version": "3.958.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.958.0.tgz", + "integrity": "sha512-vdoZbNG2dt66I7EpN3fKCzi6fp9xjIiwEA/vVVgqO4wXCGw8rKPIdDUus4e13VvTr330uQs2W0UNg/7AgtquEQ==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@aws-sdk/credential-provider-env": "3.957.0", + "@aws-sdk/credential-provider-http": "3.957.0", + "@aws-sdk/credential-provider-ini": "3.958.0", + "@aws-sdk/credential-provider-process": "3.957.0", + "@aws-sdk/credential-provider-sso": "3.958.0", + "@aws-sdk/credential-provider-web-identity": "3.958.0", + "@aws-sdk/types": "3.957.0", + "@smithy/credential-provider-imds": "^4.2.7", + "@smithy/property-provider": "^4.2.7", + "@smithy/shared-ini-file-loader": "^4.4.2", + "@smithy/types": "^4.11.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-process": { + "version": "3.957.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.957.0.tgz", + "integrity": "sha512-/KIz9kadwbeLy6SKvT79W81Y+hb/8LMDyeloA2zhouE28hmne+hLn0wNCQXAAupFFlYOAtZR2NTBs7HBAReJlg==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@aws-sdk/core": "3.957.0", + "@aws-sdk/types": "3.957.0", + "@smithy/property-provider": "^4.2.7", + "@smithy/shared-ini-file-loader": "^4.4.2", + "@smithy/types": "^4.11.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-sso": { + "version": "3.958.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.958.0.tgz", + "integrity": "sha512-CBYHJ5ufp8HC4q+o7IJejCUctJXWaksgpmoFpXerbjAso7/Fg7LLUu9inXVOxlHKLlvYekDXjIUBXDJS2WYdgg==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@aws-sdk/client-sso": "3.958.0", + "@aws-sdk/core": "3.957.0", + "@aws-sdk/token-providers": "3.958.0", + "@aws-sdk/types": "3.957.0", + "@smithy/property-provider": "^4.2.7", + "@smithy/shared-ini-file-loader": "^4.4.2", + "@smithy/types": "^4.11.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-web-identity": { + "version": "3.958.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.958.0.tgz", + "integrity": "sha512-dgnvwjMq5Y66WozzUzxNkCFap+umHUtqMMKlr8z/vl9NYMLem/WUbWNpFFOVFWquXikc+ewtpBMR4KEDXfZ+KA==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@aws-sdk/core": "3.957.0", + "@aws-sdk/nested-clients": "3.958.0", + "@aws-sdk/types": "3.957.0", + "@smithy/property-provider": "^4.2.7", + "@smithy/shared-ini-file-loader": "^4.4.2", + "@smithy/types": "^4.11.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/lib-storage": { + "version": "3.958.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/lib-storage/-/lib-storage-3.958.0.tgz", + "integrity": "sha512-cd8CTiJ165ep2DKTc2PHHhVCxDn3byv10BXMGn+lkDY3KwMoatcgZ1uhFWCBuJvsCUnSExqGouJN/Q0qgjkWtg==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/abort-controller": "^4.2.7", + "@smithy/middleware-endpoint": "^4.4.1", + "@smithy/smithy-client": "^4.10.2", + "buffer": "5.6.0", + "events": "3.3.0", + "stream-browserify": "3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "@aws-sdk/client-s3": "^3.958.0" + } + }, + "node_modules/@aws-sdk/lib-storage/node_modules/buffer": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.6.0.tgz", + "integrity": "sha512-/gDYp/UtU0eA1ys8bOs9J6a+E/KWIY+DZ+Q2WESNUA0jFRsJOc0SNUO6xJ5SGA1xueg3NL65W6s+NY5l9cunuw==", + "license": "MIT", + "dependencies": { + "base64-js": "^1.0.2", + "ieee754": "^1.1.4" + } + }, + "node_modules/@aws-sdk/lib-storage/node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "license": "MIT", + "engines": { + "node": ">=0.8.x" + } + }, + "node_modules/@aws-sdk/middleware-bucket-endpoint": { + "version": "3.957.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-bucket-endpoint/-/middleware-bucket-endpoint-3.957.0.tgz", + "integrity": "sha512-iczcn/QRIBSpvsdAS/rbzmoBpleX1JBjXvCynMbDceVLBIcVrwT1hXECrhtIC2cjh4HaLo9ClAbiOiWuqt+6MA==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@aws-sdk/types": "3.957.0", + "@aws-sdk/util-arn-parser": "3.957.0", + "@smithy/node-config-provider": "^4.3.7", + "@smithy/protocol-http": "^5.3.7", + "@smithy/types": "^4.11.0", + "@smithy/util-config-provider": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/middleware-expect-continue": { + "version": "3.957.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-expect-continue/-/middleware-expect-continue-3.957.0.tgz", + "integrity": "sha512-AlbK3OeVNwZZil0wlClgeI/ISlOt/SPUxBsIns876IFaVu/Pj3DgImnYhpcJuFRek4r4XM51xzIaGQXM6GDHGg==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@aws-sdk/types": "3.957.0", + "@smithy/protocol-http": "^5.3.7", + "@smithy/types": "^4.11.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/middleware-flexible-checksums": { + "version": "3.957.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-flexible-checksums/-/middleware-flexible-checksums-3.957.0.tgz", + "integrity": "sha512-iJpeVR5V8se1hl2pt+k8bF/e9JO4KWgPCMjg8BtRspNtKIUGy7j6msYvbDixaKZaF2Veg9+HoYcOhwnZumjXSA==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@aws-crypto/crc32": "5.2.0", + "@aws-crypto/crc32c": "5.2.0", + "@aws-crypto/util": "5.2.0", + "@aws-sdk/core": "3.957.0", + "@aws-sdk/crc64-nvme": "3.957.0", + "@aws-sdk/types": "3.957.0", + "@smithy/is-array-buffer": "^4.2.0", + "@smithy/node-config-provider": "^4.3.7", + "@smithy/protocol-http": "^5.3.7", + "@smithy/types": "^4.11.0", + "@smithy/util-middleware": "^4.2.7", + "@smithy/util-stream": "^4.5.8", + "@smithy/util-utf8": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/middleware-host-header": { + "version": "3.957.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-host-header/-/middleware-host-header-3.957.0.tgz", + "integrity": "sha512-BBgKawVyfQZglEkNTuBBdC3azlyqNXsvvN4jPkWAiNYcY0x1BasaJFl+7u/HisfULstryweJq/dAvIZIxzlZaA==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@aws-sdk/types": "3.957.0", + "@smithy/protocol-http": "^5.3.7", + "@smithy/types": "^4.11.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/middleware-location-constraint": { + "version": "3.957.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-location-constraint/-/middleware-location-constraint-3.957.0.tgz", + "integrity": "sha512-y8/W7TOQpmDJg/fPYlqAhwA4+I15LrS7TwgUEoxogtkD8gfur9wFMRLT8LCyc9o4NMEcAnK50hSb4+wB0qv6tQ==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@aws-sdk/types": "3.957.0", + "@smithy/types": "^4.11.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/middleware-logger": { + "version": "3.957.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-logger/-/middleware-logger-3.957.0.tgz", + "integrity": "sha512-w1qfKrSKHf9b5a8O76yQ1t69u6NWuBjr5kBX+jRWFx/5mu6RLpqERXRpVJxfosbep7k3B+DSB5tZMZ82GKcJtQ==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@aws-sdk/types": "3.957.0", + "@smithy/types": "^4.11.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/middleware-recursion-detection": { + "version": "3.957.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.957.0.tgz", + "integrity": "sha512-D2H/WoxhAZNYX+IjkKTdOhOkWQaK0jjJrDBj56hKjU5c9ltQiaX/1PqJ4dfjHntEshJfu0w+E6XJ+/6A6ILBBA==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@aws-sdk/types": "3.957.0", + "@aws/lambda-invoke-store": "^0.2.2", + "@smithy/protocol-http": "^5.3.7", + "@smithy/types": "^4.11.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/middleware-sdk-s3": { + "version": "3.957.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-sdk-s3/-/middleware-sdk-s3-3.957.0.tgz", + "integrity": "sha512-5B2qY2nR2LYpxoQP0xUum5A1UNvH2JQpLHDH1nWFNF/XetV7ipFHksMxPNhtJJ6ARaWhQIDXfOUj0jcnkJxXUg==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@aws-sdk/core": "3.957.0", + "@aws-sdk/types": "3.957.0", + "@aws-sdk/util-arn-parser": "3.957.0", + "@smithy/core": "^3.20.0", + "@smithy/node-config-provider": "^4.3.7", + "@smithy/protocol-http": "^5.3.7", + "@smithy/signature-v4": "^5.3.7", + "@smithy/smithy-client": "^4.10.2", + "@smithy/types": "^4.11.0", + "@smithy/util-config-provider": "^4.2.0", + "@smithy/util-middleware": "^4.2.7", + "@smithy/util-stream": "^4.5.8", + "@smithy/util-utf8": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/middleware-ssec": { + "version": "3.957.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-ssec/-/middleware-ssec-3.957.0.tgz", + "integrity": "sha512-qwkmrK0lizdjNt5qxl4tHYfASh8DFpHXM1iDVo+qHe+zuslfMqQEGRkzxS8tJq/I+8F0c6v3IKOveKJAfIvfqQ==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@aws-sdk/types": "3.957.0", + "@smithy/types": "^4.11.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/middleware-user-agent": { + "version": "3.957.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.957.0.tgz", + "integrity": "sha512-50vcHu96XakQnIvlKJ1UoltrFODjsq2KvtTgHiPFteUS884lQnK5VC/8xd1Msz/1ONpLMzdCVproCQqhDTtMPQ==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@aws-sdk/core": "3.957.0", + "@aws-sdk/types": "3.957.0", + "@aws-sdk/util-endpoints": "3.957.0", + "@smithy/core": "^3.20.0", + "@smithy/protocol-http": "^5.3.7", + "@smithy/types": "^4.11.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/nested-clients": { + "version": "3.958.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.958.0.tgz", + "integrity": "sha512-/KuCcS8b5TpQXkYOrPLYytrgxBhv81+5pChkOlhegbeHttjM69pyUpQVJqyfDM/A7wPLnDrzCAnk4zaAOkY0Nw==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "3.957.0", + "@aws-sdk/middleware-host-header": "3.957.0", + "@aws-sdk/middleware-logger": "3.957.0", + "@aws-sdk/middleware-recursion-detection": "3.957.0", + "@aws-sdk/middleware-user-agent": "3.957.0", + "@aws-sdk/region-config-resolver": "3.957.0", + "@aws-sdk/types": "3.957.0", + "@aws-sdk/util-endpoints": "3.957.0", + "@aws-sdk/util-user-agent-browser": "3.957.0", + "@aws-sdk/util-user-agent-node": "3.957.0", + "@smithy/config-resolver": "^4.4.5", + "@smithy/core": "^3.20.0", + "@smithy/fetch-http-handler": "^5.3.8", + "@smithy/hash-node": "^4.2.7", + "@smithy/invalid-dependency": "^4.2.7", + "@smithy/middleware-content-length": "^4.2.7", + "@smithy/middleware-endpoint": "^4.4.1", + "@smithy/middleware-retry": "^4.4.17", + "@smithy/middleware-serde": "^4.2.8", + "@smithy/middleware-stack": "^4.2.7", + "@smithy/node-config-provider": "^4.3.7", + "@smithy/node-http-handler": "^4.4.7", + "@smithy/protocol-http": "^5.3.7", + "@smithy/smithy-client": "^4.10.2", + "@smithy/types": "^4.11.0", + "@smithy/url-parser": "^4.2.7", + "@smithy/util-base64": "^4.3.0", + "@smithy/util-body-length-browser": "^4.2.0", + "@smithy/util-body-length-node": "^4.2.1", + "@smithy/util-defaults-mode-browser": "^4.3.16", + "@smithy/util-defaults-mode-node": "^4.2.19", + "@smithy/util-endpoints": "^3.2.7", + "@smithy/util-middleware": "^4.2.7", + "@smithy/util-retry": "^4.2.7", + "@smithy/util-utf8": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/region-config-resolver": { + "version": "3.957.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/region-config-resolver/-/region-config-resolver-3.957.0.tgz", + "integrity": "sha512-V8iY3blh8l2iaOqXWW88HbkY5jDoWjH56jonprG/cpyqqCnprvpMUZWPWYJoI8rHRf2bqzZeql1slxG6EnKI7A==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@aws-sdk/types": "3.957.0", + "@smithy/config-resolver": "^4.4.5", + "@smithy/node-config-provider": "^4.3.7", + "@smithy/types": "^4.11.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/signature-v4-multi-region": { + "version": "3.957.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/signature-v4-multi-region/-/signature-v4-multi-region-3.957.0.tgz", + "integrity": "sha512-t6UfP1xMUigMMzHcb7vaZcjv7dA2DQkk9C/OAP1dKyrE0vb4lFGDaTApi17GN6Km9zFxJthEMUbBc7DL0hq1Bg==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@aws-sdk/middleware-sdk-s3": "3.957.0", + "@aws-sdk/types": "3.957.0", + "@smithy/protocol-http": "^5.3.7", + "@smithy/signature-v4": "^5.3.7", + "@smithy/types": "^4.11.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/token-providers": { + "version": "3.958.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.958.0.tgz", + "integrity": "sha512-UCj7lQXODduD1myNJQkV+LYcGYJ9iiMggR8ow8Hva1g3A/Na5imNXzz6O67k7DAee0TYpy+gkNw+SizC6min8Q==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@aws-sdk/core": "3.957.0", + "@aws-sdk/nested-clients": "3.958.0", + "@aws-sdk/types": "3.957.0", + "@smithy/property-provider": "^4.2.7", + "@smithy/shared-ini-file-loader": "^4.4.2", + "@smithy/types": "^4.11.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/types": { + "version": "3.957.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.957.0.tgz", + "integrity": "sha512-wzWC2Nrt859ABk6UCAVY/WYEbAd7FjkdrQL6m24+tfmWYDNRByTJ9uOgU/kw9zqLCAwb//CPvrJdhqjTznWXAg==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@smithy/types": "^4.11.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/util-arn-parser": { + "version": "3.957.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-arn-parser/-/util-arn-parser-3.957.0.tgz", + "integrity": "sha512-Aj6m+AyrhWyg8YQ4LDPg2/gIfGHCEcoQdBt5DeSFogN5k9mmJPOJ+IAmNSWmWRjpOxEy6eY813RNDI6qS97M0g==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/util-endpoints": { + "version": "3.957.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.957.0.tgz", + "integrity": "sha512-xwF9K24mZSxcxKS3UKQFeX/dPYkEps9wF1b+MGON7EvnbcucrJGyQyK1v1xFPn1aqXkBTFi+SZaMRx5E5YCVFw==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@aws-sdk/types": "3.957.0", + "@smithy/types": "^4.11.0", + "@smithy/url-parser": "^4.2.7", + "@smithy/util-endpoints": "^3.2.7", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/util-locate-window": { + "version": "3.957.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-locate-window/-/util-locate-window-3.957.0.tgz", + "integrity": "sha512-nhmgKHnNV9K+i9daumaIz8JTLsIIML9PE/HUks5liyrjUzenjW/aHoc7WJ9/Td/gPZtayxFnXQSJRb/fDlBuJw==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/util-user-agent-browser": { + "version": "3.957.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.957.0.tgz", + "integrity": "sha512-exueuwxef0lUJRnGaVkNSC674eAiWU07ORhxBnevFFZEKisln+09Qrtw823iyv5I1N8T+wKfh95xvtWQrNKNQw==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@aws-sdk/types": "3.957.0", + "@smithy/types": "^4.11.0", + "bowser": "^2.11.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-sdk/util-user-agent-node": { + "version": "3.957.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.957.0.tgz", + "integrity": "sha512-ycbYCwqXk4gJGp0Oxkzf2KBeeGBdTxz559D41NJP8FlzSej1Gh7Rk40Zo6AyTfsNWkrl/kVi1t937OIzC5t+9Q==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@aws-sdk/middleware-user-agent": "3.957.0", + "@aws-sdk/types": "3.957.0", + "@smithy/node-config-provider": "^4.3.7", + "@smithy/types": "^4.11.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "aws-crt": ">=1.0.0" + }, + "peerDependenciesMeta": { + "aws-crt": { + "optional": true + } + } + }, + "node_modules/@aws-sdk/xml-builder": { + "version": "3.957.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.957.0.tgz", + "integrity": "sha512-Ai5iiQqS8kJ5PjzMhWcLKN0G2yasAkvpnPlq2EnqlIMdB48HsizElt62qcktdxp4neRMyGkFq4NzgmDbXnhRiA==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@smithy/types": "^4.11.0", + "fast-xml-parser": "5.2.5", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws/lambda-invoke-store": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/@aws/lambda-invoke-store/-/lambda-invoke-store-0.2.2.tgz", + "integrity": "sha512-C0NBLsIqzDIae8HFw9YIrIBsbc0xTiOtt7fAukGPnqQ/+zZNaq+4jhuccltK0QuWHBnNm/a6kLIRA6GFiM10eg==", + "license": "Apache-2.0", + "peer": true, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", + "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.27.1", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.4.tgz", + "integrity": "sha512-YsmSKC29MJwf0gF8Rjjrg5LQCmyh+j/nD8/eP7f+BeoQTKYqs9RoWbjGOdy0+1Ekr68RJZMUOPVQaQisnIo4Rw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.4.tgz", + "integrity": "sha512-2BCOP7TN8M+gVDj7/ht3hsaO/B/n5oDbiAyyvnRlNOs+u1o+JWNYTQrmpuNp1/Wq2gcFrI01JAW+paEKDMx/CA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.3", + "@babel/helper-compilation-targets": "^7.27.2", + "@babel/helper-module-transforms": "^7.28.3", + "@babel/helpers": "^7.28.4", + "@babel/parser": "^7.28.4", + "@babel/template": "^7.27.2", + "@babel/traverse": "^7.28.4", + "@babel/types": "^7.28.4", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/core/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/generator": { + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.3.tgz", + "integrity": "sha512-3lSpxGgvnmZznmBkCRnVREPUFJv2wrv9iAoFDvADJc0ypmdOxdUtcLeBgBJ6zE0PMeTKnxeQzyk0xTBq4Ep7zw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.28.3", + "@babel/types": "^7.28.2", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz", + "integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.27.2", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz", + "integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.3.tgz", + "integrity": "sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1", + "@babel/traverse": "^7.28.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz", + "integrity": "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz", + "integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.4.tgz", + "integrity": "sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.4" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.4.tgz", + "integrity": "sha512-yZbBqeM6TkpP9du/I2pUZnJsRMGGvOuIrhjzC1AwHwW+6he4mni6Bp/m8ijn0iOuZuPI2BfkCoSRunpyjnrQKg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.4" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-syntax-async-generators": { + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", + "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-bigint": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz", + "integrity": "sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-properties": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", + "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.12.13" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-static-block": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz", + "integrity": "sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-attributes": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.27.1.tgz", + "integrity": "sha512-oFT0FrKHgF53f4vOsZGi2Hh3I35PfSmVs4IBFLFj4dnafP+hIWDLg3VyKmUHfLoLHlyxY4C7DGtmHuJgn+IGww==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-meta": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", + "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-json-strings": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", + "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-jsx": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.27.1.tgz", + "integrity": "sha512-y8YTNIeKoyhGd9O0Jiyzyyqk8gdjnumGTQPsz0xOZOQ2RmkVJeZ1vmmfIvFEKqucBG6axJGBZDE/7iI5suUI/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-logical-assignment-operators": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", + "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-nullish-coalescing-operator": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", + "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-numeric-separator": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", + "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-object-rest-spread": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", + "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-catch-binding": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", + "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-chaining": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", + "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-private-property-in-object": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz", + "integrity": "sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-top-level-await": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", + "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-typescript": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.27.1.tgz", + "integrity": "sha512-xfYCBMxveHrRMnAWl1ZlPXOZjzkN82THFvLhQhFXFt81Z5HnN+EtUkZhv/zcKpmT3fzmWZB0ywiBrbC3vogbwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", + "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/parser": "^7.27.2", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.4.tgz", + "integrity": "sha512-YEzuboP2qvQavAcjgQNVgsvHIDv6ZpwXvcvjmyySP2DIMuByS/6ioU5G9pYrWHM6T2YDfc7xga9iNzYOs12CFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.3", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.28.4", + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.4", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.4.tgz", + "integrity": "sha512-bkFqkLhh3pMBUQQkpVgWDWq/lqzc2678eUyDlTBhRqhCHFguYYGM0Efga7tYk4TogG/3x0EEl66/OQ+WGbWB/Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@bcoe/v8-coverage": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", + "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.9.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.0.tgz", + "integrity": "sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.1", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz", + "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.21.0", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.0.tgz", + "integrity": "sha512-ENIdc4iLu0d93HeYirvKmrzshzofPw6VkZRKQGe9Nv46ZnWUzcF1xV01dcvEg/1wXUR61OmmlSfyeyO7EvjLxQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^2.1.6", + "debug": "^4.3.1", + "minimatch": "^3.1.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.0.tgz", + "integrity": "sha512-WUFvV4WoIwW8Bv0KeKCIIEgdSiFOsulyN0xrMu+7z43q/hkOLXjvb5u7UC9jDxvRzcrbEmuZBX5yJZz1741jog==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.16.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/core": { + "version": "0.16.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.16.0.tgz", + "integrity": "sha512-nmC8/totwobIiFcGkDza3GIKfAw1+hLiYVrh3I1nIomQ8PEr5cxg34jnkmGawul/ep52wGRAcyeDCNtWKSOj4Q==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.1.tgz", + "integrity": "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/js": { + "version": "9.37.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.37.0.tgz", + "integrity": "sha512-jaS+NJ+hximswBG6pjNX0uEJZkrT0zwpVi3BA3vX22aFGjJjmgSTSmPpZCRKmoBL5VY/M6p0xsSJx7rk7sy5gg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + } + }, + "node_modules/@eslint/object-schema": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.6.tgz", + "integrity": "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.0.tgz", + "integrity": "sha512-sB5uyeq+dwCWyPi31B2gQlVlo+j5brPlWx4yZBrEaRo/nhdDE8Xke1gsGgtiBdaBTxuTkceLVuVt/pclrasb0A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.16.0", + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", + "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.7", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.7.tgz", + "integrity": "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.1", + "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@ioredis/commands": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@ioredis/commands/-/commands-1.4.0.tgz", + "integrity": "sha512-aFT2yemJJo+TZCmieA7qnYGQooOS7QfNmYrzGtsYd3g9j5iDP8AimYYAesf79ohjbLG12XxC4nG5DyEnC88AsQ==", + "license": "MIT" + }, + "node_modules/@istanbuljs/load-nyc-config": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", + "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "camelcase": "^5.3.1", + "find-up": "^4.1.0", + "get-package-type": "^0.1.0", + "js-yaml": "^3.13.1", + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "license": "MIT", + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/js-yaml": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", + "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/schema": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", + "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/console": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/console/-/console-29.7.0.tgz", + "integrity": "sha512-5Ni4CU7XHQi32IJ398EEP4RrB8eV09sXP2ROqD4bksHrnTree52PsxvX8tpL8LvTZ3pFzXyPbNQReSN41CAhOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/core": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/core/-/core-29.7.0.tgz", + "integrity": "sha512-n7aeXWKMnGtDA48y8TLWJPJmLmmZ642Ceo78cYWEpiD7FzDgmNDV/GCVRorPABdXLJZ/9wzzgZAlHjXjxDHGsg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/reporters": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "ansi-escapes": "^4.2.1", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.9", + "jest-changed-files": "^29.7.0", + "jest-config": "^29.7.0", + "jest-haste-map": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-resolve-dependencies": "^29.7.0", + "jest-runner": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "jest-watcher": "^29.7.0", + "micromatch": "^4.0.4", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/@jest/environment": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-29.7.0.tgz", + "integrity": "sha512-aQIfHDq33ExsN4jP1NWGXhxgQ/wixs60gDiKO+XVMd8Mn0NWPWgc34ZQDTb2jKaUWQ7MuwoitXAsN2XVXNMpAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/fake-timers": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-mock": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/expect": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-29.7.0.tgz", + "integrity": "sha512-8uMeAMycttpva3P1lBHB8VciS9V0XAr3GymPpipdyQXbBcuhkLQOSe8E/p92RyAdToS6ZD1tFkX+CkhoECE0dQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "expect": "^29.7.0", + "jest-snapshot": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/expect-utils": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-29.7.0.tgz", + "integrity": "sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "jest-get-type": "^29.6.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/fake-timers": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-29.7.0.tgz", + "integrity": "sha512-q4DH1Ha4TTFPdxLsqDXK1d3+ioSL7yL5oCMJZgDYm6i+6CygW5E5xVr/D1HdsGxjt1ZWSfUAs9OxSB/BNelWrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@sinonjs/fake-timers": "^10.0.2", + "@types/node": "*", + "jest-message-util": "^29.7.0", + "jest-mock": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/globals": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-29.7.0.tgz", + "integrity": "sha512-mpiz3dutLbkW2MNFubUGUEVLkTGiqW6yLVTA+JbP6fI6J5iL9Y0Nlg8k95pcF8ctKwCS7WVxteBs29hhfAotzQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/expect": "^29.7.0", + "@jest/types": "^29.6.3", + "jest-mock": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/reporters": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-29.7.0.tgz", + "integrity": "sha512-DApq0KJbJOEzAFYjHADNNxAE3KbhxQB1y5Kplb5Waqw6zVbuWatSnMjE5gs8FUgEPmNsnZA3NCWl9NG0ia04Pg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@bcoe/v8-coverage": "^0.2.3", + "@jest/console": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@jridgewell/trace-mapping": "^0.3.18", + "@types/node": "*", + "chalk": "^4.0.0", + "collect-v8-coverage": "^1.0.0", + "exit": "^0.1.2", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "istanbul-lib-coverage": "^3.0.0", + "istanbul-lib-instrument": "^6.0.0", + "istanbul-lib-report": "^3.0.0", + "istanbul-lib-source-maps": "^4.0.0", + "istanbul-reports": "^3.1.3", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "jest-worker": "^29.7.0", + "slash": "^3.0.0", + "string-length": "^4.0.1", + "strip-ansi": "^6.0.0", + "v8-to-istanbul": "^9.0.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/@jest/schemas": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", + "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sinclair/typebox": "^0.27.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/source-map": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-29.6.3.tgz", + "integrity": "sha512-MHjT95QuipcPrpLM+8JMSzFx6eHp5Bm+4XeFDJlwsvVBjmKNiIAvasGK2fxz2WbGRlnvqehFbh07MMa7n3YJnw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.18", + "callsites": "^3.0.0", + "graceful-fs": "^4.2.9" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/test-result": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-29.7.0.tgz", + "integrity": "sha512-Fdx+tv6x1zlkJPcWXmMDAG2HBnaR9XPSd5aDWQVsfrZmLVT3lU1cwyxLgRmXR9yrq4NBoEm9BMsfgFzTQAbJYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "collect-v8-coverage": "^1.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/test-sequencer": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-29.7.0.tgz", + "integrity": "sha512-GQwJ5WZVrKnOJuiYiAF52UNUJXgTZx1NHjFSEB0qEMmSZKAkdMoIzw/Cj6x6NF4AvV23AUqDpFzQkN/eYCYTxw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/test-result": "^29.7.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/transform": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-29.7.0.tgz", + "integrity": "sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.11.6", + "@jest/types": "^29.6.3", + "@jridgewell/trace-mapping": "^0.3.18", + "babel-plugin-istanbul": "^6.1.1", + "chalk": "^4.0.0", + "convert-source-map": "^2.0.0", + "fast-json-stable-stringify": "^2.1.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-util": "^29.7.0", + "micromatch": "^4.0.4", + "pirates": "^4.0.4", + "slash": "^3.0.0", + "write-file-atomic": "^4.0.2" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/types": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", + "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^17.0.8", + "chalk": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@mapbox/node-pre-gyp": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.11.tgz", + "integrity": "sha512-Yhlar6v9WQgUp/He7BdgzOz8lqMQ8sU+jkCq7Wx8Myc5YFJLbEe7lgui/V7G1qB1DJykHSGwreceSaD60Y0PUQ==", + "license": "BSD-3-Clause", + "dependencies": { + "detect-libc": "^2.0.0", + "https-proxy-agent": "^5.0.0", + "make-dir": "^3.1.0", + "node-fetch": "^2.6.7", + "nopt": "^5.0.0", + "npmlog": "^5.0.1", + "rimraf": "^3.0.2", + "semver": "^7.3.5", + "tar": "^6.1.11" + }, + "bin": { + "node-pre-gyp": "bin/node-pre-gyp" + } + }, + "node_modules/@mongodb-js/saslprep": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@mongodb-js/saslprep/-/saslprep-1.3.1.tgz", + "integrity": "sha512-6nZrq5kfAz0POWyhljnbWQQJQ5uT8oE2ddX303q1uY0tWsivWKgBDXBBvuFPwOqRRalXJuVO9EjOdVtuhLX0zg==", + "license": "MIT", + "dependencies": { + "sparse-bitfield": "^3.0.3" + } + }, + "node_modules/@msgpackr-extract/msgpackr-extract-darwin-arm64": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-darwin-arm64/-/msgpackr-extract-darwin-arm64-3.0.3.tgz", + "integrity": "sha512-QZHtlVgbAdy2zAqNA9Gu1UpIuI8Xvsd1v8ic6B2pZmeFnFcMWiPLfWXh7TVw4eGEZ/C9TH281KwhVoeQUKbyjw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-darwin-x64": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-darwin-x64/-/msgpackr-extract-darwin-x64-3.0.3.tgz", + "integrity": "sha512-mdzd3AVzYKuUmiWOQ8GNhl64/IoFGol569zNRdkLReh6LRLHOXxU4U8eq0JwaD8iFHdVGqSy4IjFL4reoWCDFw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-linux-arm": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-arm/-/msgpackr-extract-linux-arm-3.0.3.tgz", + "integrity": "sha512-fg0uy/dG/nZEXfYilKoRe7yALaNmHoYeIoJuJ7KJ+YyU2bvY8vPv27f7UKhGRpY6euFYqEVhxCFZgAUNQBM3nw==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-linux-arm64": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-arm64/-/msgpackr-extract-linux-arm64-3.0.3.tgz", + "integrity": "sha512-YxQL+ax0XqBJDZiKimS2XQaf+2wDGVa1enVRGzEvLLVFeqa5kx2bWbtcSXgsxjQB7nRqqIGFIcLteF/sHeVtQg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-linux-x64": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-x64/-/msgpackr-extract-linux-x64-3.0.3.tgz", + "integrity": "sha512-cvwNfbP07pKUfq1uH+S6KJ7dT9K8WOE4ZiAcsrSes+UY55E/0jLYc+vq+DO7jlmqRb5zAggExKm0H7O/CBaesg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-win32-x64": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-win32-x64/-/msgpackr-extract-win32-x64-3.0.3.tgz", + "integrity": "sha512-x0fWaQtYp4E6sktbsdAqnehxDgEc/VwM7uLsRCYWaiGu0ykYdZPiS8zCWdnjHwyiumousxfBm4SO31eXqwEZhQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@noble/hashes": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", + "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@paralleldrive/cuid2": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/@paralleldrive/cuid2/-/cuid2-2.2.2.tgz", + "integrity": "sha512-ZOBkgDwEdoYVlSeRbYYXs0S9MejQofiVYoTbKzy/6GQa39/q5tQU2IX46+shYnUkpEl3wc+J6wRlar7r2EK2xA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@noble/hashes": "^1.1.5" + } + }, + "node_modules/@prisma/client": { + "version": "5.22.0", + "resolved": "https://registry.npmjs.org/@prisma/client/-/client-5.22.0.tgz", + "integrity": "sha512-M0SVXfyHnQREBKxCgyo7sffrKttwE6R8PMq330MIUF0pTwjUhLbW84pFDlf06B27XyCR++VtjugEnIHdr07SVA==", + "hasInstallScript": true, + "license": "Apache-2.0", + "engines": { + "node": ">=16.13" + }, + "peerDependencies": { + "prisma": "*" + }, + "peerDependenciesMeta": { + "prisma": { + "optional": true + } + } + }, + "node_modules/@prisma/debug": { + "version": "5.22.0", + "resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-5.22.0.tgz", + "integrity": "sha512-AUt44v3YJeggO2ZU5BkXI7M4hu9BF2zzH2iF2V5pyXT/lRTyWiElZ7It+bRH1EshoMRxHgpYg4VB6rCM+mG5jQ==", + "license": "Apache-2.0" + }, + "node_modules/@prisma/engines": { + "version": "5.22.0", + "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-5.22.0.tgz", + "integrity": "sha512-UNjfslWhAt06kVL3CjkuYpHAWSO6L4kDCVPegV6itt7nD1kSJavd3vhgAEhjglLJJKEdJ7oIqDJ+yHk6qO8gPA==", + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@prisma/debug": "5.22.0", + "@prisma/engines-version": "5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2", + "@prisma/fetch-engine": "5.22.0", + "@prisma/get-platform": "5.22.0" + } + }, + "node_modules/@prisma/engines-version": { + "version": "5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2", + "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2.tgz", + "integrity": "sha512-2PTmxFR2yHW/eB3uqWtcgRcgAbG1rwG9ZriSvQw+nnb7c4uCr3RAcGMb6/zfE88SKlC1Nj2ziUvc96Z379mHgQ==", + "license": "Apache-2.0" + }, + "node_modules/@prisma/fetch-engine": { + "version": "5.22.0", + "resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-5.22.0.tgz", + "integrity": "sha512-bkrD/Mc2fSvkQBV5EpoFcZ87AvOgDxbG99488a5cexp5Ccny+UM6MAe/UFkUC0wLYD9+9befNOqGiIJhhq+HbA==", + "license": "Apache-2.0", + "dependencies": { + "@prisma/debug": "5.22.0", + "@prisma/engines-version": "5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2", + "@prisma/get-platform": "5.22.0" + } + }, + "node_modules/@prisma/get-platform": { + "version": "5.22.0", + "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-5.22.0.tgz", + "integrity": "sha512-pHhpQdr1UPFpt+zFfnPazhulaZYCUqeIcPpJViYoq9R+D/yw4fjE+CtnsnKzPYm0ddUbeXUzjGVGIRVgPDCk4Q==", + "license": "Apache-2.0", + "dependencies": { + "@prisma/debug": "5.22.0" + } + }, + "node_modules/@sinclair/typebox": { + "version": "0.27.8", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", + "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@sinonjs/commons": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", + "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "type-detect": "4.0.8" + } + }, + "node_modules/@sinonjs/fake-timers": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-10.3.0.tgz", + "integrity": "sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@sinonjs/commons": "^3.0.0" + } + }, + "node_modules/@smithy/abort-controller": { + "version": "4.2.7", + "resolved": "https://registry.npmjs.org/@smithy/abort-controller/-/abort-controller-4.2.7.tgz", + "integrity": "sha512-rzMY6CaKx2qxrbYbqjXWS0plqEy7LOdKHS0bg4ixJ6aoGDPNUcLWk/FRNuCILh7GKLG9TFUXYYeQQldMBBwuyw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.11.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/chunked-blob-reader": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@smithy/chunked-blob-reader/-/chunked-blob-reader-5.2.0.tgz", + "integrity": "sha512-WmU0TnhEAJLWvfSeMxBNe5xtbselEO8+4wG0NtZeL8oR21WgH1xiO37El+/Y+H/Ie4SCwBy3MxYWmOYaGgZueA==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/chunked-blob-reader-native": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@smithy/chunked-blob-reader-native/-/chunked-blob-reader-native-4.2.1.tgz", + "integrity": "sha512-lX9Ay+6LisTfpLid2zZtIhSEjHMZoAR5hHCR4H7tBz/Zkfr5ea8RcQ7Tk4mi0P76p4cN+Btz16Ffno7YHpKXnQ==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@smithy/util-base64": "^4.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/config-resolver": { + "version": "4.4.5", + "resolved": "https://registry.npmjs.org/@smithy/config-resolver/-/config-resolver-4.4.5.tgz", + "integrity": "sha512-HAGoUAFYsUkoSckuKbCPayECeMim8pOu+yLy1zOxt1sifzEbrsRpYa+mKcMdiHKMeiqOibyPG0sFJnmaV/OGEg==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@smithy/node-config-provider": "^4.3.7", + "@smithy/types": "^4.11.0", + "@smithy/util-config-provider": "^4.2.0", + "@smithy/util-endpoints": "^3.2.7", + "@smithy/util-middleware": "^4.2.7", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/core": { + "version": "3.20.0", + "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.20.0.tgz", + "integrity": "sha512-WsSHCPq/neD5G/MkK4csLI5Y5Pkd9c1NMfpYEKeghSGaD4Ja1qLIohRQf2D5c1Uy5aXp76DeKHkzWZ9KAlHroQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/middleware-serde": "^4.2.8", + "@smithy/protocol-http": "^5.3.7", + "@smithy/types": "^4.11.0", + "@smithy/util-base64": "^4.3.0", + "@smithy/util-body-length-browser": "^4.2.0", + "@smithy/util-middleware": "^4.2.7", + "@smithy/util-stream": "^4.5.8", + "@smithy/util-utf8": "^4.2.0", + "@smithy/uuid": "^1.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/credential-provider-imds": { + "version": "4.2.7", + "resolved": "https://registry.npmjs.org/@smithy/credential-provider-imds/-/credential-provider-imds-4.2.7.tgz", + "integrity": "sha512-CmduWdCiILCRNbQWFR0OcZlUPVtyE49Sr8yYL0rZQ4D/wKxiNzBNS/YHemvnbkIWj623fplgkexUd/c9CAKdoA==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@smithy/node-config-provider": "^4.3.7", + "@smithy/property-provider": "^4.2.7", + "@smithy/types": "^4.11.0", + "@smithy/url-parser": "^4.2.7", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/eventstream-codec": { + "version": "4.2.7", + "resolved": "https://registry.npmjs.org/@smithy/eventstream-codec/-/eventstream-codec-4.2.7.tgz", + "integrity": "sha512-DrpkEoM3j9cBBWhufqBwnbbn+3nf1N9FP6xuVJ+e220jbactKuQgaZwjwP5CP1t+O94brm2JgVMD2atMGX3xIQ==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@aws-crypto/crc32": "5.2.0", + "@smithy/types": "^4.11.0", + "@smithy/util-hex-encoding": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/eventstream-serde-browser": { + "version": "4.2.7", + "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-browser/-/eventstream-serde-browser-4.2.7.tgz", + "integrity": "sha512-ujzPk8seYoDBmABDE5YqlhQZAXLOrtxtJLrbhHMKjBoG5b4dK4i6/mEU+6/7yXIAkqOO8sJ6YxZl+h0QQ1IJ7g==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@smithy/eventstream-serde-universal": "^4.2.7", + "@smithy/types": "^4.11.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/eventstream-serde-config-resolver": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-config-resolver/-/eventstream-serde-config-resolver-4.3.7.tgz", + "integrity": "sha512-x7BtAiIPSaNaWuzm24Q/mtSkv+BrISO/fmheiJ39PKRNH3RmH2Hph/bUKSOBOBC9unqfIYDhKTHwpyZycLGPVQ==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@smithy/types": "^4.11.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/eventstream-serde-node": { + "version": "4.2.7", + "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-node/-/eventstream-serde-node-4.2.7.tgz", + "integrity": "sha512-roySCtHC5+pQq5lK4be1fZ/WR6s/AxnPaLfCODIPArtN2du8s5Ot4mKVK3pPtijL/L654ws592JHJ1PbZFF6+A==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@smithy/eventstream-serde-universal": "^4.2.7", + "@smithy/types": "^4.11.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/eventstream-serde-universal": { + "version": "4.2.7", + "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-universal/-/eventstream-serde-universal-4.2.7.tgz", + "integrity": "sha512-QVD+g3+icFkThoy4r8wVFZMsIP08taHVKjE6Jpmz8h5CgX/kk6pTODq5cht0OMtcapUx+xrPzUTQdA+TmO0m1g==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@smithy/eventstream-codec": "^4.2.7", + "@smithy/types": "^4.11.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/fetch-http-handler": { + "version": "5.3.8", + "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-5.3.8.tgz", + "integrity": "sha512-h/Fi+o7mti4n8wx1SR6UHWLaakwHRx29sizvp8OOm7iqwKGFneT06GCSFhml6Bha5BT6ot5pj3CYZnCHhGC2Rg==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/protocol-http": "^5.3.7", + "@smithy/querystring-builder": "^4.2.7", + "@smithy/types": "^4.11.0", + "@smithy/util-base64": "^4.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/hash-blob-browser": { + "version": "4.2.8", + "resolved": "https://registry.npmjs.org/@smithy/hash-blob-browser/-/hash-blob-browser-4.2.8.tgz", + "integrity": "sha512-07InZontqsM1ggTCPSRgI7d8DirqRrnpL7nIACT4PW0AWrgDiHhjGZzbAE5UtRSiU0NISGUYe7/rri9ZeWyDpw==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@smithy/chunked-blob-reader": "^5.2.0", + "@smithy/chunked-blob-reader-native": "^4.2.1", + "@smithy/types": "^4.11.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/hash-node": { + "version": "4.2.7", + "resolved": "https://registry.npmjs.org/@smithy/hash-node/-/hash-node-4.2.7.tgz", + "integrity": "sha512-PU/JWLTBCV1c8FtB8tEFnY4eV1tSfBc7bDBADHfn1K+uRbPgSJ9jnJp0hyjiFN2PMdPzxsf1Fdu0eo9fJ760Xw==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@smithy/types": "^4.11.0", + "@smithy/util-buffer-from": "^4.2.0", + "@smithy/util-utf8": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/hash-stream-node": { + "version": "4.2.7", + "resolved": "https://registry.npmjs.org/@smithy/hash-stream-node/-/hash-stream-node-4.2.7.tgz", + "integrity": "sha512-ZQVoAwNYnFMIbd4DUc517HuwNelJUY6YOzwqrbcAgCnVn+79/OK7UjwA93SPpdTOpKDVkLIzavWm/Ck7SmnDPQ==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@smithy/types": "^4.11.0", + "@smithy/util-utf8": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/invalid-dependency": { + "version": "4.2.7", + "resolved": "https://registry.npmjs.org/@smithy/invalid-dependency/-/invalid-dependency-4.2.7.tgz", + "integrity": "sha512-ncvgCr9a15nPlkhIUx3CU4d7E7WEuVJOV7fS7nnK2hLtPK9tYRBkMHQbhXU1VvvKeBm/O0x26OEoBq+ngFpOEQ==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@smithy/types": "^4.11.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/is-array-buffer": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-4.2.0.tgz", + "integrity": "sha512-DZZZBvC7sjcYh4MazJSGiWMI2L7E0oCiRHREDzIxi/M2LY79/21iXt6aPLHge82wi5LsuRF5A06Ds3+0mlh6CQ==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/md5-js": { + "version": "4.2.7", + "resolved": "https://registry.npmjs.org/@smithy/md5-js/-/md5-js-4.2.7.tgz", + "integrity": "sha512-Wv6JcUxtOLTnxvNjDnAiATUsk8gvA6EeS8zzHig07dotpByYsLot+m0AaQEniUBjx97AC41MQR4hW0baraD1Xw==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@smithy/types": "^4.11.0", + "@smithy/util-utf8": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/middleware-content-length": { + "version": "4.2.7", + "resolved": "https://registry.npmjs.org/@smithy/middleware-content-length/-/middleware-content-length-4.2.7.tgz", + "integrity": "sha512-GszfBfCcvt7kIbJ41LuNa5f0wvQCHhnGx/aDaZJCCT05Ld6x6U2s0xsc/0mBFONBZjQJp2U/0uSJ178OXOwbhg==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@smithy/protocol-http": "^5.3.7", + "@smithy/types": "^4.11.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/middleware-endpoint": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/@smithy/middleware-endpoint/-/middleware-endpoint-4.4.1.tgz", + "integrity": "sha512-gpLspUAoe6f1M6H0u4cVuFzxZBrsGZmjx2O9SigurTx4PbntYa4AJ+o0G0oGm1L2oSX6oBhcGHwrfJHup2JnJg==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.20.0", + "@smithy/middleware-serde": "^4.2.8", + "@smithy/node-config-provider": "^4.3.7", + "@smithy/shared-ini-file-loader": "^4.4.2", + "@smithy/types": "^4.11.0", + "@smithy/url-parser": "^4.2.7", + "@smithy/util-middleware": "^4.2.7", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/middleware-retry": { + "version": "4.4.17", + "resolved": "https://registry.npmjs.org/@smithy/middleware-retry/-/middleware-retry-4.4.17.tgz", + "integrity": "sha512-MqbXK6Y9uq17h+4r0ogu/sBT6V/rdV+5NvYL7ZV444BKfQygYe8wAhDrVXagVebN6w2RE0Fm245l69mOsPGZzg==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@smithy/node-config-provider": "^4.3.7", + "@smithy/protocol-http": "^5.3.7", + "@smithy/service-error-classification": "^4.2.7", + "@smithy/smithy-client": "^4.10.2", + "@smithy/types": "^4.11.0", + "@smithy/util-middleware": "^4.2.7", + "@smithy/util-retry": "^4.2.7", + "@smithy/uuid": "^1.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/middleware-serde": { + "version": "4.2.8", + "resolved": "https://registry.npmjs.org/@smithy/middleware-serde/-/middleware-serde-4.2.8.tgz", + "integrity": "sha512-8rDGYen5m5+NV9eHv9ry0sqm2gI6W7mc1VSFMtn6Igo25S507/HaOX9LTHAS2/J32VXD0xSzrY0H5FJtOMS4/w==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/protocol-http": "^5.3.7", + "@smithy/types": "^4.11.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/middleware-stack": { + "version": "4.2.7", + "resolved": "https://registry.npmjs.org/@smithy/middleware-stack/-/middleware-stack-4.2.7.tgz", + "integrity": "sha512-bsOT0rJ+HHlZd9crHoS37mt8qRRN/h9jRve1SXUhVbkRzu0QaNYZp1i1jha4n098tsvROjcwfLlfvcFuJSXEsw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.11.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/node-config-provider": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/@smithy/node-config-provider/-/node-config-provider-4.3.7.tgz", + "integrity": "sha512-7r58wq8sdOcrwWe+klL9y3bc4GW1gnlfnFOuL7CXa7UzfhzhxKuzNdtqgzmTV+53lEp9NXh5hY/S4UgjLOzPfw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/property-provider": "^4.2.7", + "@smithy/shared-ini-file-loader": "^4.4.2", + "@smithy/types": "^4.11.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/node-http-handler": { + "version": "4.4.7", + "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.4.7.tgz", + "integrity": "sha512-NELpdmBOO6EpZtWgQiHjoShs1kmweaiNuETUpuup+cmm/xJYjT4eUjfhrXRP4jCOaAsS3c3yPsP3B+K+/fyPCQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/abort-controller": "^4.2.7", + "@smithy/protocol-http": "^5.3.7", + "@smithy/querystring-builder": "^4.2.7", + "@smithy/types": "^4.11.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/property-provider": { + "version": "4.2.7", + "resolved": "https://registry.npmjs.org/@smithy/property-provider/-/property-provider-4.2.7.tgz", + "integrity": "sha512-jmNYKe9MGGPoSl/D7JDDs1C8b3dC8f/w78LbaVfoTtWy4xAd5dfjaFG9c9PWPihY4ggMQNQSMtzU77CNgAJwmA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.11.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/protocol-http": { + "version": "5.3.7", + "resolved": "https://registry.npmjs.org/@smithy/protocol-http/-/protocol-http-5.3.7.tgz", + "integrity": "sha512-1r07pb994I20dD/c2seaZhoCuNYm0rWrvBxhCQ70brNh11M5Ml2ew6qJVo0lclB3jMIXirD4s2XRXRe7QEi0xA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.11.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/querystring-builder": { + "version": "4.2.7", + "resolved": "https://registry.npmjs.org/@smithy/querystring-builder/-/querystring-builder-4.2.7.tgz", + "integrity": "sha512-eKONSywHZxK4tBxe2lXEysh8wbBdvDWiA+RIuaxZSgCMmA0zMgoDpGLJhnyj+c0leOQprVnXOmcB4m+W9Rw7sg==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.11.0", + "@smithy/util-uri-escape": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/querystring-parser": { + "version": "4.2.7", + "resolved": "https://registry.npmjs.org/@smithy/querystring-parser/-/querystring-parser-4.2.7.tgz", + "integrity": "sha512-3X5ZvzUHmlSTHAXFlswrS6EGt8fMSIxX/c3Rm1Pni3+wYWB6cjGocmRIoqcQF9nU5OgGmL0u7l9m44tSUpfj9w==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.11.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/service-error-classification": { + "version": "4.2.7", + "resolved": "https://registry.npmjs.org/@smithy/service-error-classification/-/service-error-classification-4.2.7.tgz", + "integrity": "sha512-YB7oCbukqEb2Dlh3340/8g8vNGbs/QsNNRms+gv3N2AtZz9/1vSBx6/6tpwQpZMEJFs7Uq8h4mmOn48ZZ72MkA==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@smithy/types": "^4.11.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/shared-ini-file-loader": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/@smithy/shared-ini-file-loader/-/shared-ini-file-loader-4.4.2.tgz", + "integrity": "sha512-M7iUUff/KwfNunmrgtqBfvZSzh3bmFgv/j/t1Y1dQ+8dNo34br1cqVEqy6v0mYEgi0DkGO7Xig0AnuOaEGVlcg==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.11.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/signature-v4": { + "version": "5.3.7", + "resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-5.3.7.tgz", + "integrity": "sha512-9oNUlqBlFZFOSdxgImA6X5GFuzE7V2H7VG/7E70cdLhidFbdtvxxt81EHgykGK5vq5D3FafH//X+Oy31j3CKOg==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@smithy/is-array-buffer": "^4.2.0", + "@smithy/protocol-http": "^5.3.7", + "@smithy/types": "^4.11.0", + "@smithy/util-hex-encoding": "^4.2.0", + "@smithy/util-middleware": "^4.2.7", + "@smithy/util-uri-escape": "^4.2.0", + "@smithy/util-utf8": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/smithy-client": { + "version": "4.10.2", + "resolved": "https://registry.npmjs.org/@smithy/smithy-client/-/smithy-client-4.10.2.tgz", + "integrity": "sha512-D5z79xQWpgrGpAHb054Fn2CCTQZpog7JELbVQ6XAvXs5MNKWf28U9gzSBlJkOyMl9LA1TZEjRtwvGXfP0Sl90g==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.20.0", + "@smithy/middleware-endpoint": "^4.4.1", + "@smithy/middleware-stack": "^4.2.7", + "@smithy/protocol-http": "^5.3.7", + "@smithy/types": "^4.11.0", + "@smithy/util-stream": "^4.5.8", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/types": { + "version": "4.11.0", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.11.0.tgz", + "integrity": "sha512-mlrmL0DRDVe3mNrjTcVcZEgkFmufITfUAPBEA+AHYiIeYyJebso/He1qLbP3PssRe22KUzLRpQSdBPbXdgZ2VA==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/url-parser": { + "version": "4.2.7", + "resolved": "https://registry.npmjs.org/@smithy/url-parser/-/url-parser-4.2.7.tgz", + "integrity": "sha512-/RLtVsRV4uY3qPWhBDsjwahAtt3x2IsMGnP5W1b2VZIe+qgCqkLxI1UOHDZp1Q1QSOrdOR32MF3Ph2JfWT1VHg==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/querystring-parser": "^4.2.7", + "@smithy/types": "^4.11.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-base64": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@smithy/util-base64/-/util-base64-4.3.0.tgz", + "integrity": "sha512-GkXZ59JfyxsIwNTWFnjmFEI8kZpRNIBfxKjv09+nkAWPt/4aGaEWMM04m4sxgNVWkbt2MdSvE3KF/PfX4nFedQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/util-buffer-from": "^4.2.0", + "@smithy/util-utf8": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-body-length-browser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-body-length-browser/-/util-body-length-browser-4.2.0.tgz", + "integrity": "sha512-Fkoh/I76szMKJnBXWPdFkQJl2r9SjPt3cMzLdOB6eJ4Pnpas8hVoWPYemX/peO0yrrvldgCUVJqOAjUrOLjbxg==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-body-length-node": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@smithy/util-body-length-node/-/util-body-length-node-4.2.1.tgz", + "integrity": "sha512-h53dz/pISVrVrfxV1iqXlx5pRg3V2YWFcSQyPyXZRrZoZj4R4DeWRDo1a7dd3CPTcFi3kE+98tuNyD2axyZReA==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-buffer-from": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-4.2.0.tgz", + "integrity": "sha512-kAY9hTKulTNevM2nlRtxAG2FQ3B2OR6QIrPY3zE5LqJy1oxzmgBGsHLWTcNhWXKchgA0WHW+mZkQrng/pgcCew==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/is-array-buffer": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-config-provider": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-config-provider/-/util-config-provider-4.2.0.tgz", + "integrity": "sha512-YEjpl6XJ36FTKmD+kRJJWYvrHeUvm5ykaUS5xK+6oXffQPHeEM4/nXlZPe+Wu0lsgRUcNZiliYNh/y7q9c2y6Q==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-defaults-mode-browser": { + "version": "4.3.16", + "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-browser/-/util-defaults-mode-browser-4.3.16.tgz", + "integrity": "sha512-/eiSP3mzY3TsvUOYMeL4EqUX6fgUOj2eUOU4rMMgVbq67TiRLyxT7Xsjxq0bW3OwuzK009qOwF0L2OgJqperAQ==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@smithy/property-provider": "^4.2.7", + "@smithy/smithy-client": "^4.10.2", + "@smithy/types": "^4.11.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-defaults-mode-node": { + "version": "4.2.19", + "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-node/-/util-defaults-mode-node-4.2.19.tgz", + "integrity": "sha512-3a4+4mhf6VycEJyHIQLypRbiwG6aJvbQAeRAVXydMmfweEPnLLabRbdyo/Pjw8Rew9vjsh5WCdhmDaHkQnhhhA==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@smithy/config-resolver": "^4.4.5", + "@smithy/credential-provider-imds": "^4.2.7", + "@smithy/node-config-provider": "^4.3.7", + "@smithy/property-provider": "^4.2.7", + "@smithy/smithy-client": "^4.10.2", + "@smithy/types": "^4.11.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-endpoints": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@smithy/util-endpoints/-/util-endpoints-3.2.7.tgz", + "integrity": "sha512-s4ILhyAvVqhMDYREeTS68R43B1V5aenV5q/V1QpRQJkCXib5BPRo4s7uNdzGtIKxaPHCfU/8YkvPAEvTpxgspg==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@smithy/node-config-provider": "^4.3.7", + "@smithy/types": "^4.11.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-hex-encoding": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-hex-encoding/-/util-hex-encoding-4.2.0.tgz", + "integrity": "sha512-CCQBwJIvXMLKxVbO88IukazJD9a4kQ9ZN7/UMGBjBcJYvatpWk+9g870El4cB8/EJxfe+k+y0GmR9CAzkF+Nbw==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-middleware": { + "version": "4.2.7", + "resolved": "https://registry.npmjs.org/@smithy/util-middleware/-/util-middleware-4.2.7.tgz", + "integrity": "sha512-i1IkpbOae6NvIKsEeLLM9/2q4X+M90KV3oCFgWQI4q0Qz+yUZvsr+gZPdAEAtFhWQhAHpTsJO8DRJPuwVyln+w==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.11.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-retry": { + "version": "4.2.7", + "resolved": "https://registry.npmjs.org/@smithy/util-retry/-/util-retry-4.2.7.tgz", + "integrity": "sha512-SvDdsQyF5CIASa4EYVT02LukPHVzAgUA4kMAuZ97QJc2BpAqZfA4PINB8/KOoCXEw9tsuv/jQjMeaHFvxdLNGg==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@smithy/service-error-classification": "^4.2.7", + "@smithy/types": "^4.11.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-stream": { + "version": "4.5.8", + "resolved": "https://registry.npmjs.org/@smithy/util-stream/-/util-stream-4.5.8.tgz", + "integrity": "sha512-ZnnBhTapjM0YPGUSmOs0Mcg/Gg87k503qG4zU2v/+Js2Gu+daKOJMeqcQns8ajepY8tgzzfYxl6kQyZKml6O2w==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/fetch-http-handler": "^5.3.8", + "@smithy/node-http-handler": "^4.4.7", + "@smithy/types": "^4.11.0", + "@smithy/util-base64": "^4.3.0", + "@smithy/util-buffer-from": "^4.2.0", + "@smithy/util-hex-encoding": "^4.2.0", + "@smithy/util-utf8": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-uri-escape": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-uri-escape/-/util-uri-escape-4.2.0.tgz", + "integrity": "sha512-igZpCKV9+E/Mzrpq6YacdTQ0qTiLm85gD6N/IrmyDvQFA4UnU3d5g3m8tMT/6zG/vVkWSU+VxeUyGonL62DuxA==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-utf8": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-4.2.0.tgz", + "integrity": "sha512-zBPfuzoI8xyBtR2P6WQj63Rz8i3AmfAaJLuNG8dWsfvPe8lO4aCPYLn879mEgHndZH1zQ2oXmG8O1GGzzaoZiw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/util-buffer-from": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-waiter": { + "version": "4.2.7", + "resolved": "https://registry.npmjs.org/@smithy/util-waiter/-/util-waiter-4.2.7.tgz", + "integrity": "sha512-vHJFXi9b7kUEpHWUCY3Twl+9NPOZvQ0SAi+Ewtn48mbiJk4JY9MZmKQjGB4SCvVb9WPiSphZJYY6RIbs+grrzw==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@smithy/abort-controller": "^4.2.7", + "@smithy/types": "^4.11.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/uuid": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@smithy/uuid/-/uuid-1.1.0.tgz", + "integrity": "sha512-4aUIteuyxtBUhVdiQqcDhKFitwfd9hqoSDYY2KRXiWtgoWJ9Bmise+KfEPDiVHWeJepvF8xJO9/9+WDIciMFFw==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/graceful-fs": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.9.tgz", + "integrity": "sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/istanbul-lib-coverage": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", + "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/istanbul-lib-report": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.3.tgz", + "integrity": "sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/istanbul-lib-coverage": "*" + } + }, + "node_modules/@types/istanbul-reports": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.4.tgz", + "integrity": "sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/istanbul-lib-report": "*" + } + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "24.7.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.7.0.tgz", + "integrity": "sha512-IbKooQVqUBrlzWTi79E8Fw78l8k1RNtlDDNWsFZs7XonuQSJ8oNYfEeclhprUldXISRMLzBpILuKgPlIxm+/Yw==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.14.0" + } + }, + "node_modules/@types/stack-utils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", + "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/webidl-conversions": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/@types/webidl-conversions/-/webidl-conversions-7.0.3.tgz", + "integrity": "sha512-CiJJvcRtIgzadHCYXw7dqEnMNRjhGZlYK05Mj9OyktqV8uVT8fD2BFOB7S1uwBE3Kj2Z+4UyPmFw/Ixgw/LAlA==", + "license": "MIT" + }, + "node_modules/@types/whatwg-url": { + "version": "11.0.5", + "resolved": "https://registry.npmjs.org/@types/whatwg-url/-/whatwg-url-11.0.5.tgz", + "integrity": "sha512-coYR071JRaHa+xoEvvYqvnIHaVqaYrLPbsufM9BF63HkwI5Lgmy2QR8Q5K/lYDYo5AK82wOvSOS0UsLTpTG7uQ==", + "license": "MIT", + "dependencies": { + "@types/webidl-conversions": "*" + } + }, + "node_modules/@types/yargs": { + "version": "17.0.33", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.33.tgz", + "integrity": "sha512-WpxBCKWPLr4xSsHgz511rFJAM+wS28w2zEO1QDNY5zM/S8ok70NNfztH0xwhqKyaK0OHCbN98LDAZuy1ctxDkA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/@types/yargs-parser": { + "version": "21.0.3", + "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz", + "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/abbrev": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", + "license": "ISC" + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/acorn": { + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "license": "MIT", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-escapes": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", + "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^0.21.3" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/append-field": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/append-field/-/append-field-1.0.0.tgz", + "integrity": "sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==", + "license": "MIT" + }, + "node_modules/aproba": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/aproba/-/aproba-2.1.0.tgz", + "integrity": "sha512-tLIEcj5GuR2RSTnxNKdkK0dJ/GrC7P38sUkiDmDuHfsHmbagTFAxDVIBltoklXEVIQ/f14IL8IMJ5pn9Hez1Ew==", + "license": "ISC" + }, + "node_modules/are-we-there-yet": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-2.0.0.tgz", + "integrity": "sha512-Ci/qENmwHnsYo9xKIcUJN5LeDKdJ6R1Z1j9V/J5wyq8nh/mYPEpIKJbBZXtZjG04HiK7zV/p6Vs9952MrMeUIw==", + "deprecated": "This package is no longer supported.", + "license": "ISC", + "dependencies": { + "delegates": "^1.0.0", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/are-we-there-yet/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", + "license": "MIT" + }, + "node_modules/asap": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", + "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==", + "dev": true, + "license": "MIT" + }, + "node_modules/async": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", + "license": "MIT" + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/available-typed-arrays": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", + "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", + "license": "MIT", + "dependencies": { + "possible-typed-array-names": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/aws-sdk": { + "version": "2.1693.0", + "resolved": "https://registry.npmjs.org/aws-sdk/-/aws-sdk-2.1693.0.tgz", + "integrity": "sha512-cJmb8xEnVLT+R6fBS5sn/EFJiX7tUnDaPtOPZ1vFbOJtd0fnZn/Ky2XGgsvvoeliWeH7mL3TWSX5zXXGSQV6gQ==", + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "buffer": "4.9.2", + "events": "1.1.1", + "ieee754": "1.1.13", + "jmespath": "0.16.0", + "querystring": "0.2.0", + "sax": "1.2.1", + "url": "0.10.3", + "util": "^0.12.4", + "uuid": "8.0.0", + "xml2js": "0.6.2" + }, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/aws-sdk/node_modules/uuid": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.0.0.tgz", + "integrity": "sha512-jOXGuXZAWdsTH7eZLtyXMqUb9EcWMGZNbL9YcGBJl4MH4nrxHmZJhEHvyLFrkxo+28uLb/NYRcStH48fnD0Vzw==", + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/axios": { + "version": "1.13.5", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.5.tgz", + "integrity": "sha512-cz4ur7Vb0xS4/KUN0tPWe44eqxrIu31me+fbang3ijiNscE129POzipJJA6zniq2C/Z6sJCjMimjS8Lc/GAs8Q==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.11", + "form-data": "^4.0.5", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/babel-jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.7.0.tgz", + "integrity": "sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/transform": "^29.7.0", + "@types/babel__core": "^7.1.14", + "babel-plugin-istanbul": "^6.1.1", + "babel-preset-jest": "^29.6.3", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.8.0" + } + }, + "node_modules/babel-plugin-istanbul": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz", + "integrity": "sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@babel/helper-plugin-utils": "^7.0.0", + "@istanbuljs/load-nyc-config": "^1.0.0", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-instrument": "^5.0.4", + "test-exclude": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/babel-plugin-istanbul/node_modules/istanbul-lib-instrument": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.1.tgz", + "integrity": "sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@babel/core": "^7.12.3", + "@babel/parser": "^7.14.7", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^6.3.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/babel-plugin-istanbul/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/babel-plugin-jest-hoist": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-29.6.3.tgz", + "integrity": "sha512-ESAc/RJvGTFEzRwOTT4+lNDk/GNHMkKbNzsvT0qKRfDyyYTskxB5rnU2njIDYVxXCBHHEI1c0YwHob3WaYujOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.3.3", + "@babel/types": "^7.3.3", + "@types/babel__core": "^7.1.14", + "@types/babel__traverse": "^7.0.6" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/babel-preset-current-node-syntax": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.2.0.tgz", + "integrity": "sha512-E/VlAEzRrsLEb2+dv8yp3bo4scof3l9nR4lrld+Iy5NyVqgVYUJnDAmunkhPMisRI32Qc4iRiz425d8vM++2fg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/plugin-syntax-async-generators": "^7.8.4", + "@babel/plugin-syntax-bigint": "^7.8.3", + "@babel/plugin-syntax-class-properties": "^7.12.13", + "@babel/plugin-syntax-class-static-block": "^7.14.5", + "@babel/plugin-syntax-import-attributes": "^7.24.7", + "@babel/plugin-syntax-import-meta": "^7.10.4", + "@babel/plugin-syntax-json-strings": "^7.8.3", + "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", + "@babel/plugin-syntax-numeric-separator": "^7.10.4", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", + "@babel/plugin-syntax-optional-chaining": "^7.8.3", + "@babel/plugin-syntax-private-property-in-object": "^7.14.5", + "@babel/plugin-syntax-top-level-await": "^7.14.5" + }, + "peerDependencies": { + "@babel/core": "^7.0.0 || ^8.0.0-0" + } + }, + "node_modules/babel-preset-jest": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-29.6.3.tgz", + "integrity": "sha512-0B3bhxR6snWXJZtR/RliHTDPRgn1sNHOR0yVtq/IiQFyuOVjFS+wuio/R4gSNkyYmKmJB4wGZv2NZanmKmTnNA==", + "dev": true, + "license": "MIT", + "dependencies": { + "babel-plugin-jest-hoist": "^29.6.3", + "babel-preset-current-node-syntax": "^1.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "license": "MIT" + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/baseline-browser-mapping": { + "version": "2.8.12", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.12.tgz", + "integrity": "sha512-vAPMQdnyKCBtkmQA6FMCBvU9qFIppS3nzyXnEM+Lo2IAhG4Mpjv9cCxMudhgV3YdNNJv6TNqXy97dfRVL2LmaQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.js" + } + }, + "node_modules/basic-auth": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.1.tgz", + "integrity": "sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.1.2" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/basic-auth/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, + "node_modules/bcrypt": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-5.1.1.tgz", + "integrity": "sha512-AGBHOG5hPYZ5Xl9KXzU5iKq9516yEmvCKDg3ecP5kX2aB6UqTeXZxk2ELnDgDm6BQSMlLt9rDB4LoSMx0rYwww==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "@mapbox/node-pre-gyp": "^1.0.11", + "node-addon-api": "^5.0.0" + }, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/body-parser": { + "version": "1.20.3", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", + "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==", + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "on-finished": "2.4.1", + "qs": "6.13.0", + "raw-body": "2.5.2", + "type-is": "~1.6.18", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/body-parser/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/body-parser/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/bowser": { + "version": "2.13.1", + "resolved": "https://registry.npmjs.org/bowser/-/bowser-2.13.1.tgz", + "integrity": "sha512-OHawaAbjwx6rqICCKgSG0SAnT05bzd7ppyKLVUITZpANBaaMFBAsaNkto3LoQ31tyFP5kNujE8Cdx85G9VzOkw==", + "license": "MIT", + "peer": true + }, + "node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.26.3", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.26.3.tgz", + "integrity": "sha512-lAUU+02RFBuCKQPj/P6NgjlbCnLBMp4UtgTx7vNHd3XSIJF87s9a5rA3aH2yw3GS9DqZAUbOtZdCCiZeVRqt0w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.8.9", + "caniuse-lite": "^1.0.30001746", + "electron-to-chromium": "^1.5.227", + "node-releases": "^2.0.21", + "update-browserslist-db": "^1.1.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/bser": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz", + "integrity": "sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "node-int64": "^0.4.0" + } + }, + "node_modules/bson": { + "version": "6.10.4", + "resolved": "https://registry.npmjs.org/bson/-/bson-6.10.4.tgz", + "integrity": "sha512-WIsKqkSC0ABoBJuT1LEX+2HEvNmNKKgnTAyd0fL8qzK4SH2i9NXg+t08YtdZp/V9IZ33cxe3iV4yM0qg8lMQng==", + "license": "Apache-2.0", + "engines": { + "node": ">=16.20.1" + } + }, + "node_modules/buffer": { + "version": "4.9.2", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-4.9.2.tgz", + "integrity": "sha512-xq+q3SRMOxGivLhBNaUdC64hDTQwejJ+H0T/NB1XMtTVEwNTrfFF3gAxiyW0Bu/xWEGhjVKgUcMhCrUy2+uCWg==", + "license": "MIT", + "dependencies": { + "base64-js": "^1.0.2", + "ieee754": "^1.1.4", + "isarray": "^1.0.0" + } + }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", + "license": "BSD-3-Clause" + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "license": "MIT" + }, + "node_modules/bullmq": { + "version": "5.61.0", + "resolved": "https://registry.npmjs.org/bullmq/-/bullmq-5.61.0.tgz", + "integrity": "sha512-khaTjc1JnzaYFl4FrUtsSsqugAW/urRrcZ9Q0ZE+REAw8W+gkHFqxbGlutOu6q7j7n91wibVaaNlOUMdiEvoSQ==", + "license": "MIT", + "dependencies": { + "cron-parser": "^4.9.0", + "ioredis": "^5.4.1", + "msgpackr": "^1.11.2", + "node-abort-controller": "^3.1.1", + "semver": "^7.5.4", + "tslib": "^2.0.0", + "uuid": "^11.1.0" + } + }, + "node_modules/busboy": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", + "integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==", + "dependencies": { + "streamsearch": "^1.1.0" + }, + "engines": { + "node": ">=10.16.0" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", + "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.0", + "es-define-property": "^1.0.0", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001748", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001748.tgz", + "integrity": "sha512-5P5UgAr0+aBmNiplks08JLw+AW/XG/SurlgZLgB1dDLfAw7EfRGxIwzPHxdSCGY/BTKDqIVyJL87cCN6s0ZR0w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/char-regex": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz", + "integrity": "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/chownr": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", + "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/ci-info": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", + "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/cjs-module-lexer": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.4.3.tgz", + "integrity": "sha512-9z8TZaGM1pfswYeXrUpzPrkx8UnWYdhJclsiYMm6x/w5+nN+8Tf/LnAgfLGQCm59qAOxU8WwHEq2vNwF6i4j+Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/cluster-key-slot": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.1.2.tgz", + "integrity": "sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/co": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", + "integrity": "sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">= 1.0.0", + "node": ">= 0.12.0" + } + }, + "node_modules/collect-v8-coverage": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.2.tgz", + "integrity": "sha512-lHl4d5/ONEbLlJvaJNtsF/Lz+WvB07u2ycqTYbdrq7UypDXailES4valYb2eWiJFxZlVmpGekfqoxQhzyFdT4Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/color-support": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz", + "integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==", + "license": "ISC", + "bin": { + "color-support": "bin.js" + } + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/component-emitter": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.1.tgz", + "integrity": "sha512-T0+barUSQRTUQASh8bx02dl+DhF54GtIDY13Y3m9oWTklKbb3Wv974meRpeZ3lp1JpLVECWWNHC4vaG2XHXouQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "license": "MIT" + }, + "node_modules/concat-stream": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz", + "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==", + "engines": [ + "node >= 0.8" + ], + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^2.2.2", + "typedarray": "^0.0.6" + } + }, + "node_modules/console-control-strings": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", + "integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==", + "license": "ISC" + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-parser": { + "version": "1.4.7", + "resolved": "https://registry.npmjs.org/cookie-parser/-/cookie-parser-1.4.7.tgz", + "integrity": "sha512-nGUvgXnotP3BsjiLX2ypbQnWoGUPIIfHQNZkkC668ntrzGWEZVW70HDEB1qnNGMicPje6EttlIgzo51YSwNQGw==", + "license": "MIT", + "dependencies": { + "cookie": "0.7.2", + "cookie-signature": "1.0.6" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", + "license": "MIT" + }, + "node_modules/cookiejar": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/cookiejar/-/cookiejar-2.1.4.tgz", + "integrity": "sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw==", + "dev": true, + "license": "MIT" + }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "license": "MIT" + }, + "node_modules/cors": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/create-jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/create-jest/-/create-jest-29.7.0.tgz", + "integrity": "sha512-Adz2bdH0Vq3F53KEMJOoftQFutWCukm6J24wbPWRO4k1kMY7gS7ds/uoJkNuV8wDCtWWnuwGcJwpWcih+zEW1Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.9", + "jest-config": "^29.7.0", + "jest-util": "^29.7.0", + "prompts": "^2.0.1" + }, + "bin": { + "create-jest": "bin/create-jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/cron-parser": { + "version": "4.9.0", + "resolved": "https://registry.npmjs.org/cron-parser/-/cron-parser-4.9.0.tgz", + "integrity": "sha512-p0SaNjrHOnQeR8/VnfGbmg9te2kfyYSQ7Sc/j/6DtPL3JQvKxmjO9TSjNFpujqV3vEYYBvNNvXSxzyksBWAx1Q==", + "license": "MIT", + "dependencies": { + "luxon": "^3.2.1" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/dedent": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.7.0.tgz", + "integrity": "sha512-HGFtf8yhuhGhqO07SV79tRp+br4MnbdjeVxotpn1QBl30pcLLCQjX5b2295ll0fv8RKDKsmWYrl05usHM9CewQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "babel-plugin-macros": "^3.1.0" + }, + "peerDependenciesMeta": { + "babel-plugin-macros": { + "optional": true + } + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/delegates": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", + "integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==", + "license": "MIT" + }, + "node_modules/denque": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz", + "integrity": "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.10" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "license": "MIT", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/detect-newline": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", + "integrity": "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/dezalgo": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/dezalgo/-/dezalgo-1.0.4.tgz", + "integrity": "sha512-rXSP0bf+5n0Qonsb+SVVfNfIsimO4HEtmnIpPHY8Q1UCzKlQrDMfdobr8nJOOsRgWCyMRqeSBQzmWUMq7zvVig==", + "dev": true, + "license": "ISC", + "dependencies": { + "asap": "^2.0.0", + "wrappy": "1" + } + }, + "node_modules/diff-sequences": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", + "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/dotenv": { + "version": "16.6.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", + "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/ejs": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz", + "integrity": "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==", + "license": "Apache-2.0", + "dependencies": { + "jake": "^10.8.5" + }, + "bin": { + "ejs": "bin/cli.js" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.232", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.232.tgz", + "integrity": "sha512-ENirSe7wf8WzyPCibqKUG1Cg43cPaxH4wRR7AJsX7MCABCHBIOFqvaYODSLKUuZdraxUTHRE/0A2Aq8BYKEHOg==", + "dev": true, + "license": "ISC" + }, + "node_modules/emittery": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.13.1.tgz", + "integrity": "sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sindresorhus/emittery?sponsor=1" + } + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/error-ex": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", + "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "9.37.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.37.0.tgz", + "integrity": "sha512-XyLmROnACWqSxiGYArdef1fItQd47weqB7iwtfr9JHwRrqIXZdcFMvvEcL9xHCmL0SNsOvF0c42lWyM1U5dgig==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.21.0", + "@eslint/config-helpers": "^0.4.0", + "@eslint/core": "^0.16.0", + "@eslint/eslintrc": "^3.3.1", + "@eslint/js": "9.37.0", + "@eslint/plugin-kit": "^0.4.0", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "@types/json-schema": "^7.0.15", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^8.4.0", + "eslint-visitor-keys": "^4.2.1", + "espree": "^10.4.0", + "esquery": "^1.5.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-scope": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", + "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.15.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true, + "license": "BSD-2-Clause", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/esquery": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", + "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/events": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/events/-/events-1.1.1.tgz", + "integrity": "sha512-kEcvvCBByWXGnZy6JUlgAp2gBIUjfCAV6P6TgT1/aaQKcmuAEC4OZTV1I4EWQLz2gxZw76atuVyvHhTxvi0Flw==", + "license": "MIT", + "engines": { + "node": ">=0.4.x" + } + }, + "node_modules/execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/exit": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", + "integrity": "sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==", + "dev": true, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/expect": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/expect/-/expect-29.7.0.tgz", + "integrity": "sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/expect-utils": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/express": { + "version": "4.21.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz", + "integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==", + "license": "MIT", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "1.20.3", + "content-disposition": "0.5.4", + "content-type": "~1.0.4", + "cookie": "0.7.1", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "1.3.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "merge-descriptors": "1.0.3", + "methods": "~1.1.2", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.12", + "proxy-addr": "~2.0.7", + "qs": "6.13.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "0.19.0", + "serve-static": "1.16.2", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express/node_modules/cookie": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", + "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/express/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-safe-stringify": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz", + "integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-xml-parser": { + "version": "5.2.5", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.2.5.tgz", + "integrity": "sha512-pfX9uG9Ki0yekDHx2SiuRIyFdyAr1kMIMitPvb0YBo8SUfKvia7w7FIyd/l6av85pFYRhZscS75MwMnbvY+hcQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "peer": true, + "dependencies": { + "strnum": "^2.1.0" + }, + "bin": { + "fxparser": "src/cli/cli.js" + } + }, + "node_modules/fb-watchman": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz", + "integrity": "sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "bser": "2.1.1" + } + }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/file-type": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/file-type/-/file-type-3.9.0.tgz", + "integrity": "sha512-RLoqTXE8/vPmMuTI88DAzhMYC99I8BWv7zYP4A1puo5HIjEJ5EX48ighy4ZyKMG9EDXxBgW6e++cn7d1xuFghA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/filelist": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.4.tgz", + "integrity": "sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==", + "license": "Apache-2.0", + "dependencies": { + "minimatch": "^5.0.1" + } + }, + "node_modules/filelist/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/filelist/node_modules/minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/finalhandler": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz", + "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "statuses": "2.0.1", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/finalhandler/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/finalhandler/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", + "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", + "dev": true, + "license": "ISC" + }, + "node_modules/follow-redirects": { + "version": "1.15.11", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", + "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/for-each": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", + "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", + "license": "MIT", + "dependencies": { + "is-callable": "^1.2.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/form-data": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/formidable": { + "version": "3.5.4", + "resolved": "https://registry.npmjs.org/formidable/-/formidable-3.5.4.tgz", + "integrity": "sha512-YikH+7CUTOtP44ZTnUhR7Ic2UASBPOqmaRkRKxRbywPTe5VxF7RRCck4af9wutiZ/QKM5nME9Bie2fFaPz5Gug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@paralleldrive/cuid2": "^2.2.2", + "dezalgo": "^1.0.4", + "once": "^1.4.0" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "url": "https://ko-fi.com/tunnckoCore/commissions" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fs-minipass": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", + "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/fs-minipass/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/fs-minipass/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "license": "ISC" + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "license": "ISC" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gauge": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/gauge/-/gauge-3.0.2.tgz", + "integrity": "sha512-+5J6MS/5XksCuXq++uFRsnUd7Ovu1XenbeuIuNRJxYWjgQbPuFhT14lAvsWfqfAmnwluf1OwMjz39HjfLPci0Q==", + "deprecated": "This package is no longer supported.", + "license": "ISC", + "dependencies": { + "aproba": "^1.0.3 || ^2.0.0", + "color-support": "^1.1.2", + "console-control-strings": "^1.0.0", + "has-unicode": "^2.0.1", + "object-assign": "^4.1.1", + "signal-exit": "^3.0.0", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1", + "wide-align": "^1.1.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/generator-function": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/generator-function/-/generator-function-2.0.1.tgz", + "integrity": "sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-package-type": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", + "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-unicode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", + "integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==", + "license": "ISC" + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/helmet": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/helmet/-/helmet-8.1.0.tgz", + "integrity": "sha512-jOiHyAZsmnr8LqoPGmCjYAaiuWwjAPLgY8ZX2XrmHawt99/u1y6RgrZMTeoPfpUbV96HOalYgz1qzkRbw54Pmg==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/html-comment-regex": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/html-comment-regex/-/html-comment-regex-1.1.2.tgz", + "integrity": "sha512-P+M65QY2JQ5Y0G9KKdlDpo0zK+/OHptU5AaBwUfAIDJZk1MYf32Frm84EcOytfJE0t5JvkAnKlmjsXDnWzCJmQ==", + "license": "MIT" + }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true, + "license": "MIT" + }, + "node_modules/http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "license": "MIT", + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "license": "MIT", + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/human-signals": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=10.17.0" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ieee754": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.13.tgz", + "integrity": "sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg==", + "license": "BSD-3-Clause" + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/ignore-by-default": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz", + "integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==", + "dev": true, + "license": "ISC" + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/import-local": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.2.0.tgz", + "integrity": "sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pkg-dir": "^4.2.0", + "resolve-cwd": "^3.0.0" + }, + "bin": { + "import-local-fixture": "fixtures/cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ioredis": { + "version": "5.8.1", + "resolved": "https://registry.npmjs.org/ioredis/-/ioredis-5.8.1.tgz", + "integrity": "sha512-Qho8TgIamqEPdgiMadJwzRMW3TudIg6vpg4YONokGDudy4eqRIJtDbVX72pfLBcWxvbn3qm/40TyGUObdW4tLQ==", + "license": "MIT", + "dependencies": { + "@ioredis/commands": "1.4.0", + "cluster-key-slot": "^1.1.0", + "debug": "^4.3.4", + "denque": "^2.1.0", + "lodash.defaults": "^4.2.0", + "lodash.isarguments": "^3.1.0", + "redis-errors": "^1.2.0", + "redis-parser": "^3.0.0", + "standard-as-callback": "^2.1.0" + }, + "engines": { + "node": ">=12.22.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/ioredis" + } + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-arguments": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.2.0.tgz", + "integrity": "sha512-7bVbi0huj/wrIAOzb8U1aszg9kdi3KN/CyU19CTI7tAoZYEZoL9yCDXpbXN+uPsuWnP02cyug1gleqq+TU+YCA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "dev": true, + "license": "MIT" + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-callable": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-generator-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-generator-fn/-/is-generator-fn-2.1.0.tgz", + "integrity": "sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/is-generator-function": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.2.tgz", + "integrity": "sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.4", + "generator-function": "^2.0.0", + "get-proto": "^1.0.1", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-regex": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", + "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-typed-array": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", + "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", + "license": "MIT", + "dependencies": { + "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "license": "MIT" + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-instrument": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.3.tgz", + "integrity": "sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@babel/core": "^7.23.9", + "@babel/parser": "^7.23.9", + "@istanbuljs/schema": "^0.1.3", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-report/node_modules/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/istanbul-lib-source-maps": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz", + "integrity": "sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0", + "source-map": "^0.6.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-reports": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz", + "integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jake": { + "version": "10.9.4", + "resolved": "https://registry.npmjs.org/jake/-/jake-10.9.4.tgz", + "integrity": "sha512-wpHYzhxiVQL+IV05BLE2Xn34zW1S223hvjtqk0+gsPrwd/8JNLXJgZZM/iPFsYc1xyphF+6M6EvdE5E9MBGkDA==", + "license": "Apache-2.0", + "dependencies": { + "async": "^3.2.6", + "filelist": "^1.0.4", + "picocolors": "^1.1.1" + }, + "bin": { + "jake": "bin/cli.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest/-/jest-29.7.0.tgz", + "integrity": "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/core": "^29.7.0", + "@jest/types": "^29.6.3", + "import-local": "^3.0.2", + "jest-cli": "^29.7.0" + }, + "bin": { + "jest": "bin/jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/jest-changed-files": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-29.7.0.tgz", + "integrity": "sha512-fEArFiwf1BpQ+4bXSprcDc3/x4HSzL4al2tozwVpDFpsxALjLYdyiIK4e5Vz66GQJIbXJ82+35PtysofptNX2w==", + "dev": true, + "license": "MIT", + "dependencies": { + "execa": "^5.0.0", + "jest-util": "^29.7.0", + "p-limit": "^3.1.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-circus": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-29.7.0.tgz", + "integrity": "sha512-3E1nCMgipcTkCocFwM90XXQab9bS+GMsjdpmPrlelaxwD93Ad8iVEjX/vvHPdLPnFf+L40u+5+iutRdA1N9myw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/expect": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "co": "^4.6.0", + "dedent": "^1.0.0", + "is-generator-fn": "^2.0.0", + "jest-each": "^29.7.0", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "p-limit": "^3.1.0", + "pretty-format": "^29.7.0", + "pure-rand": "^6.0.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-cli": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-29.7.0.tgz", + "integrity": "sha512-OVVobw2IubN/GSYsxETi+gOe7Ka59EFMR/twOU3Jb2GnKKeMGJB5SGUUrEz3SFVmJASUdZUzy83sLNNQ2gZslg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/core": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "create-jest": "^29.7.0", + "exit": "^0.1.2", + "import-local": "^3.0.2", + "jest-config": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "yargs": "^17.3.1" + }, + "bin": { + "jest": "bin/jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/jest-config": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-29.7.0.tgz", + "integrity": "sha512-uXbpfeQ7R6TZBqI3/TxCU4q4ttk3u0PJeC+E0zbfSoSjq6bJ7buBPxzQPL0ifrkY4DNu4JUdk0ImlBUYi840eQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.11.6", + "@jest/test-sequencer": "^29.7.0", + "@jest/types": "^29.6.3", + "babel-jest": "^29.7.0", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "deepmerge": "^4.2.2", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "jest-circus": "^29.7.0", + "jest-environment-node": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-runner": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "micromatch": "^4.0.4", + "parse-json": "^5.2.0", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@types/node": "*", + "ts-node": ">=9.0.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "ts-node": { + "optional": true + } + } + }, + "node_modules/jest-diff": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.7.0.tgz", + "integrity": "sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.0.0", + "diff-sequences": "^29.6.3", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-docblock": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-29.7.0.tgz", + "integrity": "sha512-q617Auw3A612guyaFgsbFeYpNP5t2aoUNLwBUbc/0kD1R4t9ixDbyFTHd1nok4epoVFpr7PmeWHrhvuV3XaJ4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "detect-newline": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-each": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-29.7.0.tgz", + "integrity": "sha512-gns+Er14+ZrEoC5fhOfYCY1LOHHr0TI+rQUHZS8Ttw2l7gl+80eHc/gFf2Ktkw0+SIACDTeWvpFcv3B04VembQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "jest-get-type": "^29.6.3", + "jest-util": "^29.7.0", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-environment-node": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-29.7.0.tgz", + "integrity": "sha512-DOSwCRqXirTOyheM+4d5YZOrWcdu0LNZ87ewUoywbcb2XR4wKgqiG8vNeYwhjFMbEkfju7wx2GYH0P2gevGvFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/fake-timers": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-mock": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-get-type": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz", + "integrity": "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-haste-map": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-29.7.0.tgz", + "integrity": "sha512-fP8u2pyfqx0K1rGn1R9pyE0/KTn+G7PxktWidOBTqFPLYX0b9ksaMFkhK5vrS3DVun09pckLdlx90QthlW7AmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/graceful-fs": "^4.1.3", + "@types/node": "*", + "anymatch": "^3.0.3", + "fb-watchman": "^2.0.0", + "graceful-fs": "^4.2.9", + "jest-regex-util": "^29.6.3", + "jest-util": "^29.7.0", + "jest-worker": "^29.7.0", + "micromatch": "^4.0.4", + "walker": "^1.0.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "optionalDependencies": { + "fsevents": "^2.3.2" + } + }, + "node_modules/jest-leak-detector": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-29.7.0.tgz", + "integrity": "sha512-kYA8IJcSYtST2BY9I+SMC32nDpBT3J2NvWJx8+JCuCdl/CR1I4EKUJROiP8XtCcxqgTTBGJNdbB1A8XRKbTetw==", + "dev": true, + "license": "MIT", + "dependencies": { + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-matcher-utils": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-29.7.0.tgz", + "integrity": "sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.0.0", + "jest-diff": "^29.7.0", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-message-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.7.0.tgz", + "integrity": "sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.12.13", + "@jest/types": "^29.6.3", + "@types/stack-utils": "^2.0.0", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "micromatch": "^4.0.4", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-mock": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-29.7.0.tgz", + "integrity": "sha512-ITOMZn+UkYS4ZFh83xYAOzWStloNzJFO2s8DWrE4lhtGD+AorgnbkiKERe4wQVBydIGPx059g6riW5Btp6Llnw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-pnp-resolver": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz", + "integrity": "sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "peerDependencies": { + "jest-resolve": "*" + }, + "peerDependenciesMeta": { + "jest-resolve": { + "optional": true + } + } + }, + "node_modules/jest-regex-util": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-29.6.3.tgz", + "integrity": "sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-resolve": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-29.7.0.tgz", + "integrity": "sha512-IOVhZSrg+UvVAshDSDtHyFCCBUl/Q3AAJv8iZ6ZjnZ74xzvwuzLXid9IIIPgTnY62SJjfuupMKZsZQRsCvxEgA==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-pnp-resolver": "^1.2.2", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "resolve": "^1.20.0", + "resolve.exports": "^2.0.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-resolve-dependencies": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-29.7.0.tgz", + "integrity": "sha512-un0zD/6qxJ+S0et7WxeI3H5XSe9lTBBR7bOHCHXkKR6luG5mwDDlIzVQ0V5cZCuoTgEdcdwzTghYkTWfubi+nA==", + "dev": true, + "license": "MIT", + "dependencies": { + "jest-regex-util": "^29.6.3", + "jest-snapshot": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-runner": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-29.7.0.tgz", + "integrity": "sha512-fsc4N6cPCAahybGBfTRcq5wFR6fpLznMg47sY5aDpsoejOcVYFb07AHuSnR0liMcPTgBsA3ZJL6kFOjPdoNipQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/environment": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "emittery": "^0.13.1", + "graceful-fs": "^4.2.9", + "jest-docblock": "^29.7.0", + "jest-environment-node": "^29.7.0", + "jest-haste-map": "^29.7.0", + "jest-leak-detector": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-resolve": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-util": "^29.7.0", + "jest-watcher": "^29.7.0", + "jest-worker": "^29.7.0", + "p-limit": "^3.1.0", + "source-map-support": "0.5.13" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-runtime": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-29.7.0.tgz", + "integrity": "sha512-gUnLjgwdGqW7B4LvOIkbKs9WGbn+QLqRQQ9juC6HndeDiezIwhDP+mhMwHWCEcfQ5RUXa6OPnFF8BJh5xegwwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/fake-timers": "^29.7.0", + "@jest/globals": "^29.7.0", + "@jest/source-map": "^29.6.3", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "cjs-module-lexer": "^1.0.0", + "collect-v8-coverage": "^1.0.0", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-mock": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "slash": "^3.0.0", + "strip-bom": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-snapshot": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-29.7.0.tgz", + "integrity": "sha512-Rm0BMWtxBcioHr1/OX5YCP8Uov4riHvKPknOGs804Zg9JGZgmIBkbtlxJC/7Z4msKYVbIJtfU+tKb8xlYNfdkw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.11.6", + "@babel/generator": "^7.7.2", + "@babel/plugin-syntax-jsx": "^7.7.2", + "@babel/plugin-syntax-typescript": "^7.7.2", + "@babel/types": "^7.3.3", + "@jest/expect-utils": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "babel-preset-current-node-syntax": "^1.0.0", + "chalk": "^4.0.0", + "expect": "^29.7.0", + "graceful-fs": "^4.2.9", + "jest-diff": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "natural-compare": "^1.4.0", + "pretty-format": "^29.7.0", + "semver": "^7.5.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", + "integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "graceful-fs": "^4.2.9", + "picomatch": "^2.2.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-validate": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-29.7.0.tgz", + "integrity": "sha512-ZB7wHqaRGVw/9hST/OuFUReG7M8vKeq0/J2egIGLdvjHCmYqGARhzXmtgi+gVeZ5uXFF219aOc3Ls2yLg27tkw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "camelcase": "^6.2.0", + "chalk": "^4.0.0", + "jest-get-type": "^29.6.3", + "leven": "^3.1.0", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-validate/node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/jest-watcher": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-29.7.0.tgz", + "integrity": "sha512-49Fg7WXkU3Vl2h6LbLtMQ/HyB6rXSIX7SqvBLQmssRBGN9I0PNvPmAmCWSOY6SOvrjhI/F7/bGAv9RtnsPA03g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "ansi-escapes": "^4.2.1", + "chalk": "^4.0.0", + "emittery": "^0.13.1", + "jest-util": "^29.7.0", + "string-length": "^4.0.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-worker": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-29.7.0.tgz", + "integrity": "sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "jest-util": "^29.7.0", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-worker/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/jiti": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz", + "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, + "node_modules/jmespath": { + "version": "0.16.0", + "resolved": "https://registry.npmjs.org/jmespath/-/jmespath-0.16.0.tgz", + "integrity": "sha512-9FzQjJ7MATs1tSpnco1K6ayiYE3figslrXA72G2HQ/n76RzvYlofyi5QM+iX4YRs/pu3yzxlVQSST23+dMDknw==", + "license": "Apache-2.0", + "engines": { + "node": ">= 0.6.0" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/jsonwebtoken": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz", + "integrity": "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==", + "license": "MIT", + "dependencies": { + "jws": "^3.2.2", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + } + }, + "node_modules/jwa": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.2.tgz", + "integrity": "sha512-eeH5JO+21J78qMvTIDdBXidBd6nG2kZjg5Ohz/1fpa28Z4CcsWUzJ1ZZyFq/3z3N17aZy+ZuBoHljASbL1WfOw==", + "license": "MIT", + "dependencies": { + "buffer-equal-constant-time": "^1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", + "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", + "license": "MIT", + "dependencies": { + "jwa": "^1.4.1", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/kareem": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/kareem/-/kareem-2.6.3.tgz", + "integrity": "sha512-C3iHfuGUXK2u8/ipq9LfjFfXFxAZMQJJq7vLS45r3D9Y2xQ/m4S8zaR4zMLFWh9AsNPXmcFfUDhTEO8UIC/V6Q==", + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/kleur": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", + "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/leven": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", + "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true, + "license": "MIT" + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash.defaults": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz", + "integrity": "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==", + "license": "MIT" + }, + "node_modules/lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==", + "license": "MIT" + }, + "node_modules/lodash.isarguments": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz", + "integrity": "sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg==", + "license": "MIT" + }, + "node_modules/lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==", + "license": "MIT" + }, + "node_modules/lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==", + "license": "MIT" + }, + "node_modules/lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==", + "license": "MIT" + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", + "license": "MIT" + }, + "node_modules/lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==", + "license": "MIT" + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", + "license": "MIT" + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/luxon": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.7.2.tgz", + "integrity": "sha512-vtEhXh/gNjI9Yg1u4jX/0YVPMvxzHuGgCm6tC5kZyb08yjGWGnqAjGJvcXbqQR2P3MyMEFnRbpcdFS6PBcLqew==", + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/make-dir": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", + "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "license": "MIT", + "dependencies": { + "semver": "^6.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/make-dir/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/makeerror": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz", + "integrity": "sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "tmpl": "1.0.5" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/memory-pager": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/memory-pager/-/memory-pager-1.5.0.tgz", + "integrity": "sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg==", + "license": "MIT" + }, + "node_modules/merge-descriptors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true, + "license": "MIT" + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/minipass": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", + "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", + "license": "ISC", + "engines": { + "node": ">=8" + } + }, + "node_modules/minizlib": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", + "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", + "license": "MIT", + "dependencies": { + "minipass": "^3.0.0", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minizlib/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minizlib/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "license": "ISC" + }, + "node_modules/mkdirp": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "license": "MIT", + "dependencies": { + "minimist": "^1.2.6" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, + "node_modules/mongodb": { + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-6.20.0.tgz", + "integrity": "sha512-Tl6MEIU3K4Rq3TSHd+sZQqRBoGlFsOgNrH5ltAcFBV62Re3Fd+FcaVf8uSEQFOJ51SDowDVttBTONMfoYWrWlQ==", + "license": "Apache-2.0", + "dependencies": { + "@mongodb-js/saslprep": "^1.3.0", + "bson": "^6.10.4", + "mongodb-connection-string-url": "^3.0.2" + }, + "engines": { + "node": ">=16.20.1" + }, + "peerDependencies": { + "@aws-sdk/credential-providers": "^3.188.0", + "@mongodb-js/zstd": "^1.1.0 || ^2.0.0", + "gcp-metadata": "^5.2.0", + "kerberos": "^2.0.1", + "mongodb-client-encryption": ">=6.0.0 <7", + "snappy": "^7.3.2", + "socks": "^2.7.1" + }, + "peerDependenciesMeta": { + "@aws-sdk/credential-providers": { + "optional": true + }, + "@mongodb-js/zstd": { + "optional": true + }, + "gcp-metadata": { + "optional": true + }, + "kerberos": { + "optional": true + }, + "mongodb-client-encryption": { + "optional": true + }, + "snappy": { + "optional": true + }, + "socks": { + "optional": true + } + } + }, + "node_modules/mongodb-connection-string-url": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mongodb-connection-string-url/-/mongodb-connection-string-url-3.0.2.tgz", + "integrity": "sha512-rMO7CGo/9BFwyZABcKAWL8UJwH/Kc2x0g72uhDWzG48URRax5TCIcJ7Rc3RZqffZzO/Gwff/jyKwCU9TN8gehA==", + "license": "Apache-2.0", + "dependencies": { + "@types/whatwg-url": "^11.0.2", + "whatwg-url": "^14.1.0 || ^13.0.0" + } + }, + "node_modules/mongoose": { + "version": "8.19.1", + "resolved": "https://registry.npmjs.org/mongoose/-/mongoose-8.19.1.tgz", + "integrity": "sha512-oB7hGQJn4f8aebqE7mhE54EReb5cxVgpCxQCQj0K/cK3q4J3Tg08nFP6sM52nJ4Hlm8jsDnhVYpqIITZUAhckQ==", + "license": "MIT", + "dependencies": { + "bson": "^6.10.4", + "kareem": "2.6.3", + "mongodb": "~6.20.0", + "mpath": "0.9.0", + "mquery": "5.0.0", + "ms": "2.1.3", + "sift": "17.1.3" + }, + "engines": { + "node": ">=16.20.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mongoose" + } + }, + "node_modules/morgan": { + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/morgan/-/morgan-1.10.1.tgz", + "integrity": "sha512-223dMRJtI/l25dJKWpgij2cMtywuG/WiUKXdvwfbhGKBhy1puASqXwFzmWZ7+K73vUPoR7SS2Qz2cI/g9MKw0A==", + "license": "MIT", + "dependencies": { + "basic-auth": "~2.0.1", + "debug": "2.6.9", + "depd": "~2.0.0", + "on-finished": "~2.3.0", + "on-headers": "~1.1.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/morgan/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/morgan/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/morgan/node_modules/on-finished": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", + "integrity": "sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/mpath": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/mpath/-/mpath-0.9.0.tgz", + "integrity": "sha512-ikJRQTk8hw5DEoFVxHG1Gn9T/xcjtdnOKIU1JTmGjZZlg9LST2mBLmcX3/ICIbgJydT2GOc15RnNy5mHmzfSew==", + "license": "MIT", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/mquery": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/mquery/-/mquery-5.0.0.tgz", + "integrity": "sha512-iQMncpmEK8R8ncT8HJGsGc9Dsp8xcgYMVSbs5jgnm1lFHTZqMJTUWTDx1LBO8+mK3tPNZWFLBghQEIOULSTHZg==", + "license": "MIT", + "dependencies": { + "debug": "4.x" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/msgpackr": { + "version": "1.11.5", + "resolved": "https://registry.npmjs.org/msgpackr/-/msgpackr-1.11.5.tgz", + "integrity": "sha512-UjkUHN0yqp9RWKy0Lplhh+wlpdt9oQBYgULZOiFhV3VclSF1JnSQWZ5r9gORQlNYaUKQoR8itv7g7z1xDDuACA==", + "license": "MIT", + "optionalDependencies": { + "msgpackr-extract": "^3.0.2" + } + }, + "node_modules/msgpackr-extract": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/msgpackr-extract/-/msgpackr-extract-3.0.3.tgz", + "integrity": "sha512-P0efT1C9jIdVRefqjzOQ9Xml57zpOXnIuS+csaB4MdZbTdmGDLo8XhzBG1N7aO11gKDDkJvBLULeFTo46wwreA==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "dependencies": { + "node-gyp-build-optional-packages": "5.2.2" + }, + "bin": { + "download-msgpackr-prebuilds": "bin/download-prebuilds.js" + }, + "optionalDependencies": { + "@msgpackr-extract/msgpackr-extract-darwin-arm64": "3.0.3", + "@msgpackr-extract/msgpackr-extract-darwin-x64": "3.0.3", + "@msgpackr-extract/msgpackr-extract-linux-arm": "3.0.3", + "@msgpackr-extract/msgpackr-extract-linux-arm64": "3.0.3", + "@msgpackr-extract/msgpackr-extract-linux-x64": "3.0.3", + "@msgpackr-extract/msgpackr-extract-win32-x64": "3.0.3" + } + }, + "node_modules/multer": { + "version": "1.4.5-lts.2", + "resolved": "https://registry.npmjs.org/multer/-/multer-1.4.5-lts.2.tgz", + "integrity": "sha512-VzGiVigcG9zUAoCNU+xShztrlr1auZOlurXynNvO9GiWD1/mTBbUljOKY+qMeazBqXgRnjzeEgJI/wyjJUHg9A==", + "deprecated": "Multer 1.x is impacted by a number of vulnerabilities, which have been patched in 2.x. You should upgrade to the latest 2.x version.", + "license": "MIT", + "dependencies": { + "append-field": "^1.0.0", + "busboy": "^1.0.0", + "concat-stream": "^1.5.2", + "mkdirp": "^0.5.4", + "object-assign": "^4.1.1", + "type-is": "^1.6.4", + "xtend": "^4.0.0" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/multer-s3": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/multer-s3/-/multer-s3-3.0.1.tgz", + "integrity": "sha512-BFwSO80a5EW4GJRBdUuSHblz2jhVSAze33ZbnGpcfEicoT0iRolx4kWR+AJV07THFRCQ78g+kelKFdjkCCaXeQ==", + "license": "MIT", + "dependencies": { + "@aws-sdk/lib-storage": "^3.46.0", + "file-type": "^3.3.0", + "html-comment-regex": "^1.1.2", + "run-parallel": "^1.1.6" + }, + "engines": { + "node": ">= 12.0.0" + }, + "peerDependencies": { + "@aws-sdk/client-s3": "^3.0.0" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/node-abort-controller": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/node-abort-controller/-/node-abort-controller-3.1.1.tgz", + "integrity": "sha512-AGK2yQKIjRuqnc6VkX2Xj5d+QW8xZ87pa1UK6yA6ouUyuxfHuMP6umE5QK7UmTeOAymo+Zx1Fxiuw9rVx8taHQ==", + "license": "MIT" + }, + "node_modules/node-addon-api": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-5.1.0.tgz", + "integrity": "sha512-eh0GgfEkpnoWDq+VY8OyvYhFEzBk6jIYbRKdIlyTiAXIVJ8PyBaKb0rp7oDtoddbdoHWhq8wwr+XZ81F1rpNdA==", + "license": "MIT" + }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/node-fetch/node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "license": "MIT" + }, + "node_modules/node-fetch/node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "license": "BSD-2-Clause" + }, + "node_modules/node-fetch/node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "license": "MIT", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/node-gyp-build-optional-packages": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/node-gyp-build-optional-packages/-/node-gyp-build-optional-packages-5.2.2.tgz", + "integrity": "sha512-s+w+rBWnpTMwSFbaE0UXsRlg7hU4FjekKU4eyAih5T8nJuNZT1nNsskXpxmeqSK9UzkBl6UgRlnKc8hz8IEqOw==", + "license": "MIT", + "optional": true, + "dependencies": { + "detect-libc": "^2.0.1" + }, + "bin": { + "node-gyp-build-optional-packages": "bin.js", + "node-gyp-build-optional-packages-optional": "optional.js", + "node-gyp-build-optional-packages-test": "build-test.js" + } + }, + "node_modules/node-int64": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", + "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-releases": { + "version": "2.0.23", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.23.tgz", + "integrity": "sha512-cCmFDMSm26S6tQSDpBCg/NR8NENrVPhAJSf+XbxBG4rPFaaonlEoE9wHQmun+cls499TQGSb7ZyPBRlzgKfpeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/nodemailer": { + "version": "7.0.11", + "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-7.0.11.tgz", + "integrity": "sha512-gnXhNRE0FNhD7wPSCGhdNh46Hs6nm+uTyg+Kq0cZukNQiYdnCsoQjodNP9BQVG9XrcK/v6/MgpAPBUFyzh9pvw==", + "license": "MIT-0", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/nodemon": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.10.tgz", + "integrity": "sha512-WDjw3pJ0/0jMFmyNDp3gvY2YizjLmmOUQo6DEBY+JgdvW/yQ9mEeSw6H5ythl5Ny2ytb7f9C2nIbjSxMNzbJXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "chokidar": "^3.5.2", + "debug": "^4", + "ignore-by-default": "^1.0.1", + "minimatch": "^3.1.2", + "pstree.remy": "^1.1.8", + "semver": "^7.5.3", + "simple-update-notifier": "^2.0.0", + "supports-color": "^5.5.0", + "touch": "^3.1.0", + "undefsafe": "^2.0.5" + }, + "bin": { + "nodemon": "bin/nodemon.js" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/nodemon" + } + }, + "node_modules/nodemon/node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/nodemon/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/nodemon/node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/nodemon/node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/nodemon/node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/nopt": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz", + "integrity": "sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==", + "license": "ISC", + "dependencies": { + "abbrev": "1" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/npmlog": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-5.0.1.tgz", + "integrity": "sha512-AqZtDUWOMKs1G/8lwylVjrdYgqA4d9nu8hc+0gzRxlDb1I10+FHBGMXs6aiQHFdCUUlqH99MUMuLfzWDNDtfxw==", + "deprecated": "This package is no longer supported.", + "license": "ISC", + "dependencies": { + "are-we-there-yet": "^2.0.0", + "console-control-strings": "^1.1.0", + "gauge": "^3.0.0", + "set-blocking": "^2.0.0" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/on-headers": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.1.0.tgz", + "integrity": "sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, + "license": "MIT" + }, + "node_modules/path-to-regexp": { + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", + "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pirates": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", + "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "find-up": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-dir/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-dir/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-dir/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pkg-dir/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/possible-typed-array-names": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", + "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/prettier": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.6.2.tgz", + "integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/prisma": { + "version": "5.22.0", + "resolved": "https://registry.npmjs.org/prisma/-/prisma-5.22.0.tgz", + "integrity": "sha512-vtpjW3XuYCSnMsNVBjLMNkTj6OZbudcPPTPYHqX0CJfpcdWciI1dM8uHETwmDxxiqEwCIE6WvXucWUetJgfu/A==", + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@prisma/engines": "5.22.0" + }, + "bin": { + "prisma": "build/index.js" + }, + "engines": { + "node": ">=16.13" + }, + "optionalDependencies": { + "fsevents": "2.3.3" + } + }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "license": "MIT" + }, + "node_modules/prompts": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", + "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "kleur": "^3.0.3", + "sisteransi": "^1.0.5" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + }, + "node_modules/pstree.remy": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz", + "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==", + "dev": true, + "license": "MIT" + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/pure-rand": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.1.0.tgz", + "integrity": "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ], + "license": "MIT" + }, + "node_modules/qs": { + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", + "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.0.6" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/querystring": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz", + "integrity": "sha512-X/xY82scca2tau62i9mDyU9K+I+djTMUsvwf7xnUX5GLvVzgJybOJf4Y6o9Zx3oJK/LSXg5tTZBjwzqVPaPO2g==", + "deprecated": "The querystring API is considered Legacy. new code should use the URLSearchParams API instead.", + "engines": { + "node": ">=0.4.x" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", + "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true, + "license": "MIT" + }, + "node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/readable-stream/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, + "node_modules/redis-errors": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/redis-errors/-/redis-errors-1.2.0.tgz", + "integrity": "sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/redis-parser": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/redis-parser/-/redis-parser-3.0.0.tgz", + "integrity": "sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A==", + "license": "MIT", + "dependencies": { + "redis-errors": "^1.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve": { + "version": "1.22.10", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", + "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-core-module": "^2.16.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-cwd": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", + "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve-cwd/node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/resolve.exports": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/resolve.exports/-/resolve.exports-2.0.3.tgz", + "integrity": "sha512-OcXjMsGdhL4XnbShKpAcSqPMzQoYkYyhbEaeSko47MjRP9NfEQMhZkXL1DoFlt9LWQn4YttrdnV6X2OiyzBi+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "license": "ISC", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safe-regex-test": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz", + "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-regex": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/sax": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.1.tgz", + "integrity": "sha512-8I2a3LovHTOpm7NV5yOyO8IHqgVsfK4+UuySrXU8YXkSRX7k6hCV9b3HrkKCr3nMpgj+0bmocaJJWpvp1oc7ZA==", + "license": "ISC" + }, + "node_modules/semver": { + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/send": { + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", + "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "2.4.1", + "range-parser": "~1.2.1", + "statuses": "2.0.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/send/node_modules/debug/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/send/node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/serve-static": { + "version": "1.16.2", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz", + "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==", + "license": "MIT", + "dependencies": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.19.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==", + "license": "ISC" + }, + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/sift": { + "version": "17.1.3", + "resolved": "https://registry.npmjs.org/sift/-/sift-17.1.3.tgz", + "integrity": "sha512-Rtlj66/b0ICeFzYTuNvX/EF1igRbbnGSvEyT79McoZa/DeGhMyC5pWKOEsZKnpkqtSeovd5FL/bjHWC3CIIvCQ==", + "license": "MIT" + }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "license": "ISC" + }, + "node_modules/simple-update-notifier": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz", + "integrity": "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/sisteransi": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", + "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", + "dev": true, + "license": "MIT" + }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.13", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.13.tgz", + "integrity": "sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/sparse-bitfield": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/sparse-bitfield/-/sparse-bitfield-3.0.3.tgz", + "integrity": "sha512-kvzhi7vqKTfkh0PZU+2D2PIllw2ymqJKujUcyPMd9Y75Nv4nPbGJZXNhxsgdQab2BmlDct1YnfQCguEvHr7VsQ==", + "license": "MIT", + "dependencies": { + "memory-pager": "^1.0.2" + } + }, + "node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/stack-utils": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", + "integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "escape-string-regexp": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/stack-utils/node_modules/escape-string-regexp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", + "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/standard-as-callback": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/standard-as-callback/-/standard-as-callback-2.1.0.tgz", + "integrity": "sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==", + "license": "MIT" + }, + "node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/stream-browserify": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/stream-browserify/-/stream-browserify-3.0.0.tgz", + "integrity": "sha512-H73RAHsVBapbim0tU2JwwOiXUj+fikfiaoYAKHF3VJfA0pe2BCzkhAHBlLG6REzE+2WNZcxOXjK7lkso+9euLA==", + "license": "MIT", + "dependencies": { + "inherits": "~2.0.4", + "readable-stream": "^3.5.0" + } + }, + "node_modules/stream-browserify/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/streamsearch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", + "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/string_decoder/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, + "node_modules/string-length": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", + "integrity": "sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "char-regex": "^1.0.2", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-bom": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", + "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/strnum": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/strnum/-/strnum-2.1.2.tgz", + "integrity": "sha512-l63NF9y/cLROq/yqKXSLtcMeeyOfnSQlfMSlzFt/K73oIaD8DGaQWd7Z34X9GPiKqP5rbSh84Hl4bOlLcjiSrQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "peer": true + }, + "node_modules/superagent": { + "version": "10.2.3", + "resolved": "https://registry.npmjs.org/superagent/-/superagent-10.2.3.tgz", + "integrity": "sha512-y/hkYGeXAj7wUMjxRbB21g/l6aAEituGXM9Rwl4o20+SX3e8YOSV6BxFXl+dL3Uk0mjSL3kCbNkwURm8/gEDig==", + "dev": true, + "license": "MIT", + "dependencies": { + "component-emitter": "^1.3.1", + "cookiejar": "^2.1.4", + "debug": "^4.3.7", + "fast-safe-stringify": "^2.1.1", + "form-data": "^4.0.4", + "formidable": "^3.5.4", + "methods": "^1.1.2", + "mime": "2.6.0", + "qs": "^6.11.2" + }, + "engines": { + "node": ">=14.18.0" + } + }, + "node_modules/superagent/node_modules/mime": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz", + "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==", + "dev": true, + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/supertest": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/supertest/-/supertest-7.1.4.tgz", + "integrity": "sha512-tjLPs7dVyqgItVFirHYqe2T+MfWc2VOBQ8QFKKbWTA3PU7liZR8zoSpAi/C1k1ilm9RsXIKYf197oap9wXGVYg==", + "dev": true, + "license": "MIT", + "dependencies": { + "methods": "^1.1.2", + "superagent": "^10.2.3" + }, + "engines": { + "node": ">=14.18.0" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/tar": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", + "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==", + "license": "ISC", + "dependencies": { + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "minipass": "^5.0.0", + "minizlib": "^2.1.1", + "mkdirp": "^1.0.3", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/tar/node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "license": "MIT", + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/tar/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "license": "ISC" + }, + "node_modules/test-exclude": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", + "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", + "dev": true, + "license": "ISC", + "dependencies": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^7.1.4", + "minimatch": "^3.0.4" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/tmpl": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", + "integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/touch": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.1.tgz", + "integrity": "sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA==", + "dev": true, + "license": "ISC", + "bin": { + "nodetouch": "bin/nodetouch.js" + } + }, + "node_modules/tr46": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.1.tgz", + "integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==", + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/type-fest": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "license": "MIT", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/typedarray": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", + "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==", + "license": "MIT" + }, + "node_modules/undefsafe": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz", + "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==", + "dev": true, + "license": "MIT" + }, + "node_modules/undici-types": { + "version": "7.14.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.14.0.tgz", + "integrity": "sha512-QQiYxHuyZ9gQUIrmPo3IA+hUl4KYk8uSA7cHrcKd/l3p1OTpZcM0Tbp9x7FAtXdAYhlasd60ncPpgu6ihG6TOA==", + "dev": true, + "license": "MIT" + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz", + "integrity": "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/url": { + "version": "0.10.3", + "resolved": "https://registry.npmjs.org/url/-/url-0.10.3.tgz", + "integrity": "sha512-hzSUW2q06EqL1gKM/a+obYHLIO6ct2hwPuviqTTOcfFVc61UbfJ2Q32+uGL/HCPxKqrdGB5QUwIe7UqlDgwsOQ==", + "license": "MIT", + "dependencies": { + "punycode": "1.3.2", + "querystring": "0.2.0" + } + }, + "node_modules/url/node_modules/punycode": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.3.2.tgz", + "integrity": "sha512-RofWgt/7fL5wP1Y7fxE7/EmTLzQVnB0ycyibJ0OOHIlJqTNzglYFxVwETOcIoJqJmpDXJ9xImDv+Fq34F/d4Dw==", + "license": "MIT" + }, + "node_modules/util": { + "version": "0.12.5", + "resolved": "https://registry.npmjs.org/util/-/util-0.12.5.tgz", + "integrity": "sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "is-arguments": "^1.0.4", + "is-generator-function": "^1.0.7", + "is-typed-array": "^1.1.3", + "which-typed-array": "^1.1.2" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/uuid": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz", + "integrity": "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/esm/bin/uuid" + } + }, + "node_modules/v8-to-istanbul": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz", + "integrity": "sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==", + "dev": true, + "license": "ISC", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.12", + "@types/istanbul-lib-coverage": "^2.0.1", + "convert-source-map": "^2.0.0" + }, + "engines": { + "node": ">=10.12.0" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/walker": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz", + "integrity": "sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "makeerror": "1.0.12" + } + }, + "node_modules/webidl-conversions": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", + "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/whatwg-url": { + "version": "14.2.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.2.0.tgz", + "integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==", + "license": "MIT", + "dependencies": { + "tr46": "^5.1.0", + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/which-typed-array": { + "version": "1.1.19", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.19.tgz", + "integrity": "sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw==", + "license": "MIT", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "for-each": "^0.3.5", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/wide-align": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz", + "integrity": "sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==", + "license": "ISC", + "dependencies": { + "string-width": "^1.0.2 || 2 || 3 || 4" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + }, + "node_modules/write-file-atomic": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-4.0.2.tgz", + "integrity": "sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==", + "dev": true, + "license": "ISC", + "dependencies": { + "imurmurhash": "^0.1.4", + "signal-exit": "^3.0.7" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/xml2js": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.6.2.tgz", + "integrity": "sha512-T4rieHaC1EXcES0Kxxj4JWgaUQHDk+qwHcYOCFHfiwKz7tOVPLq7Hjq9dM1WCMhylqMEfP7hMcOIChvotiZegA==", + "license": "MIT", + "dependencies": { + "sax": ">=0.6.0", + "xmlbuilder": "~11.0.0" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/xmlbuilder": { + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz", + "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==", + "license": "MIT", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "license": "MIT", + "engines": { + "node": ">=0.4" + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..6153811 --- /dev/null +++ b/package.json @@ -0,0 +1,69 @@ +{ + "name": "vaishnavi-backend", + "version": "1.0.0", + "description": "Vaishnavi Creation - E-commerce Backend API", + "main": "src/server.js", + "scripts": { + "start": "node src/server.js", + "dev": "nodemon src/server.js", + "build": "npm run db:generate", + "test": "jest", + "test:watch": "jest --watch", + "test:coverage": "jest --coverage", + "lint": "eslint src/", + "lint:fix": "eslint src/ --fix", + "format": "prettier --write src/", + "format:check": "prettier --check src/", + "db:generate": "prisma generate", + "db:push": "prisma db push", + "db:migrate": "prisma migrate dev", + "db:deploy": "prisma migrate deploy", + "db:studio": "prisma studio", + "db:seed": "node prisma/seed.js", + "setup": "node setup-local.js", + "test:connections": "node test-connections.js" + }, + "keywords": [ + "e-commerce", + "fashion", + "api", + "nodejs", + "express", + "postgresql", + "mongodb" + ], + "author": "Vaishnavi Creation", + "license": "ISC", + "dependencies": { + "@prisma/client": "^5.22.0", + "aws-sdk": "^2.1693.0", + "axios": "^1.13.5", + "bcrypt": "^5.1.1", + "bullmq": "^5.25.0", + "cookie-parser": "^1.4.7", + "cors": "^2.8.5", + "dotenv": "^16.4.7", + "ejs": "^3.1.10", + "express": "^4.21.2", + "helmet": "^8.0.0", + "ioredis": "^5.4.1", + "jsonwebtoken": "^9.0.2", + "mongoose": "^8.9.2", + "morgan": "^1.10.0", + "multer": "^1.4.5-lts.1", + "multer-s3": "^3.0.1", + "nodemailer": "^7.0.11", + "prisma": "^5.22.0" + }, + "devDependencies": { + "eslint": "^9.17.0", + "jest": "^29.7.0", + "nodemon": "^3.1.9", + "prettier": "^3.4.2", + "supertest": "^7.0.0" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + } +} diff --git a/prisma/schema.prisma b/prisma/schema.prisma new file mode 100644 index 0000000..25fa05b --- /dev/null +++ b/prisma/schema.prisma @@ -0,0 +1,384 @@ +// This is your Prisma schema file, +// learn more about it in the docs: https://pris.ly/d/prisma-schema + +generator client { + provider = "prisma-client-js" +} + +datasource db { + provider = "postgresql" + url = env("DATABASE_URL") +} + +// User Management & Authentication +model User { + id String @id @default(cuid()) + email String @unique + username String? @unique + firstName String? + lastName String? + phone String? + avatar String? + + // Authentication + passwordHash String + isVerified Boolean @default(false) + isActive Boolean @default(true) + + // Roles & Permissions + role UserRole @default(CUSTOMER) + + // Timestamps + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + lastLoginAt DateTime? + + // Relations + addresses Address[] + orders Order[] + reviews Review[] + wishlist WishlistItem[] + cart CartItem[] + + orderStatusChanges OrderStatusHistory[] + + + + @@map("users") +} + +enum UserRole { + CUSTOMER + ADMIN + SUPER_ADMIN + EDITOR + MODERATOR + SELLER +} + +// Address Management +model Address { + id String @id @default(cuid()) + userId String + type AddressType @default(SHIPPING) + isDefault Boolean @default(false) + + // Address Details + firstName String + lastName String + company String? + addressLine1 String + addressLine2 String? + city String + state String + postalCode String + country String + phone String? + + // Timestamps + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + // Relations + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + orders Order[] + + @@map("addresses") +} + +enum AddressType { + SHIPPING + BILLING +} + +// Order Management +model Order { + id String @id @default(cuid()) + orderNumber String @unique + userId String + status OrderStatus @default(PENDING) + + // Pricing + subtotal Decimal @db.Decimal(10, 2) + taxAmount Decimal @db.Decimal(10, 2) + shippingAmount Decimal @db.Decimal(10, 2) + discountAmount Decimal @db.Decimal(10, 2) @default(0) + totalAmount Decimal @db.Decimal(10, 2) + + // Payment + paymentStatus PaymentStatus @default(PENDING) + paymentMethod String? + paymentId String? + + // Shipping + shippingAddressId String + trackingNumber String? + + // Timestamps + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + shippedAt DateTime? + deliveredAt DateTime? + + // โœ… Add this field + returnRequestedAt DateTime? + returnStatus ReturnStatus @default(NONE) + + // Relations + user User @relation(fields: [userId], references: [id]) + address Address @relation(fields: [shippingAddressId], references: [id]) + items OrderItem[] + + statusHistory OrderStatusHistory[] + + + + @@map("orders") +} + + +model OrderStatusHistory { + id String @id @default(cuid()) + orderId String + + fromStatus OrderStatus? + toStatus OrderStatus + changedBy String? + + trackingNumber String? + notes String? + ipAddress String? + userAgent String? + + createdAt DateTime @default(now()) + + // Relations + order Order @relation(fields: [orderId], references: [id], onDelete: Cascade) + admin User? @relation(fields: [changedBy], references: [id]) + + @@index([orderId]) + @@index([createdAt]) + @@map("order_status_history") +} + + +enum OrderStatus { + PENDING + CONFIRMED + PROCESSING + SHIPPED + DELIVERED + CANCELLED + REFUNDED + RETURN_REQUESTED +} + +enum PaymentStatus { + PENDING + PAID + FAILED + REFUNDED + PARTIALLY_REFUNDED +} + +enum ReturnStatus { + NONE + REQUESTED + APPROVED + REJECTED + COMPLETED +} + + +// Order Items +model OrderItem { + id String @id @default(cuid()) + orderId String + productId String // Reference to MongoDB product + + // Product Details (snapshot at time of order) + productName String + productSku String + price Decimal @db.Decimal(10, 2) + quantity Int + + // Timestamps + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + // Relations + order Order @relation(fields: [orderId], references: [id], onDelete: Cascade) + + @@map("order_items") +} + +// Reviews & Ratings +model Review { + id String @id @default(cuid()) + userId String + productId String // Reference to MongoDB product + orderId String? // Optional reference to order + + rating Int // 1-5 stars + title String? + comment String? + isVerified Boolean @default(false) + + // Timestamps + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + // Relations + user User @relation(fields: [userId], references: [id]) + + @@unique([userId, productId]) + @@map("reviews") +} + +// Wishlist Management +model WishlistItem { + id String @id @default(cuid()) + userId String + productId String // Reference to MongoDB product + + // Timestamps + createdAt DateTime @default(now()) + + // Relations + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + + @@unique([userId, productId]) + @@map("wishlist_items") +} + +// Shopping Cart +model CartItem { + id String @id @default(cuid()) + userId String + productId String // Reference to MongoDB product + quantity Int @default(1) + + // Timestamps + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + // Relations + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + + @@unique([userId, productId]) + @@map("cart_items") +} + +// Categories (Reference data) +// Categories (Reference data) +model Category { + id String @id @default(cuid()) + name String // Removed @unique - same name can exist under different parents + slug String // Removed @unique - will use composite unique instead + description String? + image String? + parentId String? + isActive Boolean @default(true) + sequence Int @default(0) + + // SEO + metaTitle String? + metaDescription String? + + // Timestamps + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + // Relations + parent Category? @relation("CategoryHierarchy", fields: [parentId], references: [id]) + children Category[] @relation("CategoryHierarchy") + + // Composite unique constraint: same slug allowed if different parent + @@unique([slug, parentId], name: "unique_slug_per_parent") + + @@map("categories") +} + +// Coupons & Discounts +model Coupon { + id String @id @default(cuid()) + code String @unique + description String? + type CouponType + value Decimal @db.Decimal(10, 2) + + // Conditions + minOrderAmount Decimal? @db.Decimal(10, 2) + maxUses Int? + usedCount Int @default(0) + + // Validity + validFrom DateTime + validUntil DateTime + isActive Boolean @default(true) + + // Timestamps + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@map("coupons") +} + +enum CouponType { + PERCENTAGE + FIXED_AMOUNT + FREE_SHIPPING +} + +// System Configuration +model SystemConfig { + id String @id @default(cuid()) + key String @unique + value String + type ConfigType @default(STRING) + + // Timestamps + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@map("system_config") +} + +enum ConfigType { + STRING + NUMBER + BOOLEAN + JSON +} + + +model InventoryLog { + id String @id @default(cuid()) + + productId String + productName String + + type InventoryLogType + quantityChange Int + previousStock Int + newStock Int + + orderId String? + adjustedBy String? + notes String? + + createdAt DateTime @default(now()) + + @@index([productId]) + @@index([orderId]) + @@index([type]) + @@map("inventory_logs") +} + +enum InventoryLogType { + SOLD + RESTOCK + ADJUSTMENT + RETURN + DAMAGED +} \ No newline at end of file diff --git a/prisma/seed.js b/prisma/seed.js new file mode 100644 index 0000000..ad52dee --- /dev/null +++ b/prisma/seed.js @@ -0,0 +1,174 @@ +const { PrismaClient } = require('@prisma/client'); +const bcrypt = require('bcrypt'); + +const prisma = new PrismaClient(); + +async function main() { + console.log('๐ŸŒฑ Starting database seed...'); + + // Create admin user + const adminPasswordHash = await bcrypt.hash('admin123', 12); + const admin = await prisma.user.upsert({ + where: { email: 'admin@vaishnavi.com' }, + update: {}, + create: { + email: 'admin@vaishnavi.com', + passwordHash: adminPasswordHash, + firstName: 'Admin', + lastName: 'User', + username: 'admin', + role: 'ADMIN', + isVerified: true, + isActive: true, + }, + }); + + // Create test customer + const customerPasswordHash = await bcrypt.hash('customer123', 12); + const customer = await prisma.user.upsert({ + where: { email: 'customer@example.com' }, + update: {}, + create: { + email: 'customer@example.com', + passwordHash: customerPasswordHash, + firstName: 'John', + lastName: 'Doe', + username: 'johndoe', + role: 'CUSTOMER', + isVerified: true, + isActive: true, + }, + }); + + // Create categories + const categories = [ + { + name: 'Women\'s Clothing', + slug: 'womens-clothing', + description: 'Beautiful women\'s fashion and apparel', + metaTitle: 'Women\'s Clothing - Vaishnavi Creation', + metaDescription: 'Discover our collection of women\'s clothing and fashion items.', + }, + { + name: 'Men\'s Clothing', + slug: 'mens-clothing', + description: 'Stylish men\'s fashion and apparel', + metaTitle: 'Men\'s Clothing - Vaishnavi Creation', + metaDescription: 'Explore our range of men\'s clothing and fashion items.', + }, + { + name: 'Accessories', + slug: 'accessories', + description: 'Fashion accessories and jewelry', + metaTitle: 'Accessories - Vaishnavi Creation', + metaDescription: 'Complete your look with our fashion accessories.', + }, + { + name: 'Shoes', + slug: 'shoes', + description: 'Comfortable and stylish footwear', + metaTitle: 'Shoes - Vaishnavi Creation', + metaDescription: 'Find the perfect pair of shoes for any occasion.', + }, + ]; + + for (const categoryData of categories) { + await prisma.category.upsert({ + where: { slug: categoryData.slug }, + update: {}, + create: categoryData, + }); + } + + // Create sample coupons + const coupons = [ + { + code: 'WELCOME10', + description: '10% off for new customers', + type: 'PERCENTAGE', + value: 10, + minOrderAmount: 50, + maxUses: 1000, + validFrom: new Date(), + validUntil: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000), // 30 days + isActive: true, + }, + { + code: 'FREESHIP', + description: 'Free shipping on orders over $100', + type: 'FREE_SHIPPING', + value: 0, + minOrderAmount: 100, + maxUses: null, + validFrom: new Date(), + validUntil: new Date(Date.now() + 90 * 24 * 60 * 60 * 1000), // 90 days + isActive: true, + }, + ]; + + for (const couponData of coupons) { + await prisma.coupon.upsert({ + where: { code: couponData.code }, + update: {}, + create: couponData, + }); + } + + // Create system configuration + const systemConfigs = [ + { + key: 'site_name', + value: 'Vaishnavi Creation', + type: 'STRING', + }, + { + key: 'site_description', + value: 'Your premier destination for fashion and style', + type: 'STRING', + }, + { + key: 'currency', + value: 'USD', + type: 'STRING', + }, + { + key: 'tax_rate', + value: '10', + type: 'NUMBER', + }, + { + key: 'free_shipping_threshold', + value: '100', + type: 'NUMBER', + }, + { + key: 'maintenance_mode', + value: 'false', + type: 'BOOLEAN', + }, + ]; + + for (const configData of systemConfigs) { + await prisma.systemConfig.upsert({ + where: { key: configData.key }, + update: {}, + create: configData, + }); + } + + console.log('โœ… Database seeded successfully!'); + console.log(`๐Ÿ‘ค Admin user created: admin@vaishnavi.com / admin123`); + console.log(`๐Ÿ‘ค Customer user created: customer@example.com / customer123`); + console.log(`๐Ÿ“ฆ ${categories.length} categories created`); + console.log(`๐ŸŽซ ${coupons.length} coupons created`); + console.log(`โš™๏ธ ${systemConfigs.length} system configs created`); +} + +main() + .catch((e) => { + console.error('โŒ Seed failed:', e); + process.exit(1); + }) + .finally(async () => { + await prisma.$disconnect(); + }); diff --git a/setup-local.js b/setup-local.js new file mode 100644 index 0000000..1ba05f0 --- /dev/null +++ b/setup-local.js @@ -0,0 +1,101 @@ +#!/usr/bin/env node + +/** + * Local Development Setup Script + * + * This script helps set up the local development environment + * by creating the PostgreSQL database and running initial setup. + */ + +const { execSync } = require('child_process'); +const fs = require('fs'); +const path = require('path'); + +console.log('๐Ÿš€ Setting up Vaishnavi Creation Backend for local development...\n'); + +// Check if .env file exists +const envPath = path.join(__dirname, '.env'); +const envExamplePath = path.join(__dirname, '.env.example'); + +if (!fs.existsSync(envPath)) { + if (fs.existsSync(envExamplePath)) { + console.log('๐Ÿ“ Creating .env file from .env.example...'); + fs.copyFileSync(envExamplePath, envPath); + console.log('โœ… .env file created! Please update it with your database credentials.\n'); + } else { + console.log('โŒ .env.example file not found. Please create a .env file manually.\n'); + process.exit(1); + } +} + +// Check Node.js version +const nodeVersion = process.version; +const majorVersion = parseInt(nodeVersion.slice(1).split('.')[0]); + +if (majorVersion < 18) { + console.log('โŒ Node.js 18+ is required. Current version:', nodeVersion); + process.exit(1); +} + +console.log('โœ… Node.js version check passed:', nodeVersion); + +// Install dependencies +console.log('\n๐Ÿ“ฆ Installing dependencies...'); +try { + execSync('npm install', { stdio: 'inherit' }); + console.log('โœ… Dependencies installed successfully!'); +} catch (error) { + console.log('โŒ Failed to install dependencies:', error.message); + process.exit(1); +} + +// Generate Prisma client +console.log('\n๐Ÿ”ง Generating Prisma client...'); +try { + execSync('npx prisma generate', { stdio: 'inherit' }); + console.log('โœ… Prisma client generated successfully!'); +} catch (error) { + console.log('โŒ Failed to generate Prisma client:', error.message); + console.log('๐Ÿ’ก Make sure your PostgreSQL database is running and the DATABASE_URL in .env is correct.'); + process.exit(1); +} + +// Test database connection and run migrations +console.log('\n๐Ÿ—„๏ธ Setting up database...'); +try { + execSync('npx prisma db push', { stdio: 'inherit' }); + console.log('โœ… Database schema created successfully!'); +} catch (error) { + console.log('โŒ Failed to create database schema:', error.message); + console.log('๐Ÿ’ก Please check:'); + console.log(' - PostgreSQL is running'); + console.log(' - Database "vaishnavi_db" exists'); + console.log(' - DATABASE_URL in .env is correct'); + console.log(' - User has proper permissions'); + process.exit(1); +} + +// Seed database +console.log('\n๐ŸŒฑ Seeding database...'); +try { + execSync('npm run db:seed', { stdio: 'inherit' }); + console.log('โœ… Database seeded successfully!'); +} catch (error) { + console.log('โš ๏ธ Failed to seed database:', error.message); + console.log('๐Ÿ’ก You can run "npm run db:seed" manually later.'); +} + +console.log('\n๐ŸŽ‰ Setup completed successfully!'); +console.log('\n๐Ÿ“‹ Next steps:'); +console.log('1. Make sure MongoDB is running on port 27017'); +console.log('2. Update your .env file with correct database credentials'); +console.log('3. Run "npm run dev" to start the development server'); +console.log('\n๐Ÿ”— Useful commands:'); +console.log('- Start dev server: npm run dev'); +console.log('- View database: npm run db:studio'); +console.log('- Run tests: npm test'); +console.log('- Lint code: npm run lint'); +console.log('\n๐Ÿ“š API will be available at: http://localhost:3000'); +console.log('๐Ÿฅ Health check: http://localhost:3000/health'); +console.log('\n๐Ÿ‘ค Default admin user: admin@vaishnavi.com / admin123'); +console.log('๐Ÿ‘ค Default customer: customer@example.com / customer123'); diff --git a/src/config/database.js b/src/config/database.js new file mode 100644 index 0000000..e0bc94c --- /dev/null +++ b/src/config/database.js @@ -0,0 +1,57 @@ +const { PrismaClient } = require('@prisma/client'); +const mongoose = require('mongoose'); + +// PostgreSQL Connection (Prisma) +const prisma = new PrismaClient({ + log: process.env.NODE_ENV === 'development' ? ['query', 'info', 'warn', 'error'] : ['error'], +}); + +// MongoDB Connection (Mongoose) +const connectMongoDB = async () => { + try { + await mongoose.connect(process.env.MONGODB_URI); + console.log('โœ… MongoDB connected successfully'); + } catch (error) { + console.error('โŒ MongoDB connection error:', error); + process.exit(1); + } +}; + +// PostgreSQL Connection (Prisma) +const connectPostgreSQL = async () => { + try { + await prisma.$connect(); + console.log('โœ… PostgreSQL connected successfully'); + } catch (error) { + console.error('โŒ PostgreSQL connection error:', error); + process.exit(1); + } +}; + +// Initialize all database connections +const initializeDatabases = async () => { + await Promise.all([ + connectPostgreSQL(), + connectMongoDB(), + ]); +}; + +// Graceful shutdown +const closeDatabaseConnections = async () => { + try { + await Promise.all([ + prisma.$disconnect(), + mongoose.connection.close(), + ]); + console.log('โœ… Database connections closed'); + } catch (error) { + console.error('โŒ Error closing database connections:', error); + } +}; + +module.exports = { + prisma, + mongoose, + initializeDatabases, + closeDatabaseConnections, +}; diff --git a/src/config/returnPolicy.js b/src/config/returnPolicy.js new file mode 100644 index 0000000..c1d4662 --- /dev/null +++ b/src/config/returnPolicy.js @@ -0,0 +1,5 @@ +// backend/config/returnPolicy.js +module.exports = { + RETURN_WINDOW_DAYS: 7, + ALLOWED_STATUSES: ['DELIVERED'], +}; diff --git a/src/config/s3.js b/src/config/s3.js new file mode 100644 index 0000000..84a527d --- /dev/null +++ b/src/config/s3.js @@ -0,0 +1,16 @@ +// config/s3.js +// const { S3Client } from "@aws-sdk/client-s3"; +const { S3Client } = require("@aws-sdk/client-s3"); + +const s3 = new S3Client({ + endpoint: "https://s3.sahasrarameta.tech", + region: process.env.AWS_REGION, + credentials: { + accessKeyId: process.env.AWS_ACCESS_KEY_ID, + secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY, + }, + forcePathStyle: true, // IMPORTANT for MinIO +}); + +// export default s3; +module.exports = s3; diff --git a/src/controllers/admin/categoryController.js b/src/controllers/admin/categoryController.js new file mode 100644 index 0000000..bbf324f --- /dev/null +++ b/src/controllers/admin/categoryController.js @@ -0,0 +1,375 @@ +const { prisma } = require('../../config/database'); +const uploadToS3 = require('../../utils/uploadToS3'); + +exports.getAllCategories = async (req, res, next) => { + try { + const categories = await prisma.category.findMany({ + orderBy: { name: 'asc' }, + }); + // res.json({ success: true, data: categories }); + res.status(200).json({ + // statusCode: 200, + status: true, + message: 'Categories fetched successfully', + data: categories, + }); + } catch (error) { + next(error); + } +}; + +// exports.createCategory = async (req, res, next) => { +// try { +// const { name, description, image, parentId, metaTitle, metaDescription } = +// req.body; + +// // Generate slug from name +// let slug = name +// .toLowerCase() +// .replace(/[^a-z0-9]+/g, '-') +// .replace(/(^-|-$)/g, ''); + +// // Check if slug already exists with the same parent +// const existing = await prisma.category.findFirst({ +// where: { +// slug, +// parentId: parentId || null, // Handle both subcategories and root categories +// }, +// }); + +// if (existing) { +// // If exists under same parent, append timestamp to make it unique +// slug = `${slug}-${Date.now()}`; +// } + +// const category = await prisma.category.create({ +// data: { +// name, +// slug, +// description, +// image, +// parentId, +// metaTitle, +// metaDescription, +// }, +// }); + +// res.status(201).json({ +// statusCode: 201, +// status: true, +// message: 'Category created successfully', +// data: category, +// }); +// } catch (error) { +// // Handle Prisma duplicate error explicitly +// if (error.code === 'P2002') { +// return res.status(400).json({ +// statusCode: 400, +// status: false, +// message: 'Duplicate field value entered', +// }); +// } +// next(error); +// } +// }; + +// exports.updateCategory = async (req, res, next) => { +// try { +// const { id } = req.params; +// const { name, description, image, parentId, metaTitle, metaDescription } = +// req.body; + +// let slug; +// if (name) { +// slug = name +// .toLowerCase() +// .replace(/[^a-z0-9]+/g, '-') +// .replace(/(^-|-$)/g, ''); +// } + +// const category = await prisma.category.update({ +// where: { id }, +// data: { +// name, +// slug, +// description, +// image, +// parentId, +// metaTitle, +// metaDescription, +// }, +// }); + +// res.json({ +// status: true, +// message: 'Category updated successfully', +// data: category, +// }); +// } catch (error) { +// next(error); +// } +// }; + +exports.createCategory = async (req, res, next) => { + try { + const { name, description, parentId, metaTitle, metaDescription } = + req.body; + + if (!name) { + return res.status(400).json({ + status: false, + message: 'Category name is required', + }); + } + + // โœ… Generate slug + let slug = name + .toLowerCase() + .replace(/[^a-z0-9]+/g, '-') + .replace(/(^-|-$)/g, ''); + + // โœ… Check duplicate slug under same parent + const existing = await prisma.category.findFirst({ + where: { + slug, + parentId: parentId || null, + }, + }); + + if (existing) { + slug = `${slug}-${Date.now()}`; + } + + // โœ… Upload image to S3 if provided + let imageUrl = null; + + if (req.file) { + imageUrl = await uploadToS3(req.file, 'categories'); + } + + // โœ… Create category + const category = await prisma.category.create({ + data: { + name, + slug, + description, + parentId: parentId || null, + metaTitle, + metaDescription, + image: imageUrl, + }, + }); + + res.status(201).json({ + statusCode: 201, + status: true, + message: 'Category created successfully', + data: category, + }); + } catch (error) { + if (error.code === 'P2002') { + return res.status(400).json({ + statusCode: 400, + status: false, + message: 'Duplicate field value entered', + }); + } + + next(error); + } +}; + +exports.updateCategory = async (req, res, next) => { + try { + const { id } = req.params; + const { name, description, parentId, metaTitle, metaDescription } = + req.body; + + // Generate slug if name changed + let slug; + if (name) { + slug = name + .toLowerCase() + .replace(/[^a-z0-9]+/g, '-') + .replace(/(^-|-$)/g, ''); + } + + // โœ… Upload new image if provided + let imageUrl; + if (req.file) { + imageUrl = await uploadToS3(req.file, 'categories'); + } + + // Update category + const category = await prisma.category.update({ + where: { id }, + data: { + ...(name && { name }), + ...(slug && { slug }), + ...(description && { description }), + ...(parentId !== undefined && { parentId }), + ...(metaTitle && { metaTitle }), + ...(metaDescription && { metaDescription }), + ...(imageUrl && { image: imageUrl }), // ๐Ÿ‘ˆ only update if new image uploaded + }, + }); + + res.json({ + status: true, + message: 'Category updated successfully', + data: category, + }); + } catch (error) { + next(error); + } +}; + +exports.deleteCategory = async (req, res, next) => { + try { + const { id } = req.params; + + await prisma.category.delete({ + where: { id }, + }); + + res.json({ + status: true, + message: 'Category deleted successfully', + }); + } catch (error) { + next(error); + } +}; + +exports.toggleCategoryStatus = async (req, res, next) => { + try { + const { id } = req.params; + const { isActive } = req.body; + + // 1๏ธโƒฃ Update parent category + const parentCategory = await prisma.category.update({ + where: { id }, + data: { isActive }, + }); + + // 2๏ธโƒฃ If parent is being deactivated, also deactivate all children recursively + if (!isActive) { + const deactivateChildren = async parentId => { + const children = await prisma.category.findMany({ + where: { parentId }, + }); + + for (const child of children) { + await prisma.category.update({ + where: { id: child.id }, + data: { isActive: false }, + }); + // Recursive call for nested subcategories + await deactivateChildren(child.id); + } + }; + + await deactivateChildren(id); + } + + res.json({ + status: true, + message: `Category ${isActive ? 'activated' : 'deactivated'} successfully`, + data: parentCategory, + }); + } catch (error) { + next(error); + } +}; + +exports.reorderCategories = async (req, res) => { + const { orders } = req.body; + + await Promise.all( + orders.map(item => + prisma.category.update({ + where: { id: item.id }, + data: { sequence: item.sequence }, + }) + ) + ); + + res.json({ + status: true, + message: 'Category order updated', + }); +}; + +exports.getCategoryHierarchy = async (req, res, next) => { + try { + // 1. Fetch all categories + const categories = await prisma.category.findMany({ + orderBy: { name: 'asc' }, + }); + + // 2. Convert array to a lookup map + const lookup = {}; + categories.forEach(cat => { + lookup[cat.id] = { ...cat, children: [] }; + }); + + const hierarchy = []; + + // 3. Build hierarchical structure + categories.forEach(cat => { + if (cat.parentId) { + lookup[cat.parentId].children.push(lookup[cat.id]); + } else { + hierarchy.push(lookup[cat.id]); + } + }); + + res.status(200).json({ + // statusCode: 200, + status: true, + message: 'Category hierarchy fetched successfully', + data: hierarchy, + }); + } catch (error) { + next(error); + } +}; + +exports.getCategoryById = async (req, res) => { + try { + const { id } = req.params; + + if (!id) { + return res.status(400).json({ + success: false, + message: 'Category ID is required', + }); + } + + const category = await prisma.category.findUnique({ + where: { + id: id, // โœ… PASS THE ACTUAL STRING VALUE + }, + }); + + if (!category) { + return res.status(404).json({ + success: false, + message: 'Category not found', + }); + } + + return res.status(200).json({ + success: true, + message: 'Category details fetched successfully', + data: category, + }); + } catch (error) { + console.error('Get category by id error:', error); + + return res.status(500).json({ + success: false, + message: 'Error fetching category', + }); + } +}; diff --git a/src/controllers/admin/couponController.js b/src/controllers/admin/couponController.js new file mode 100644 index 0000000..d7f7b11 --- /dev/null +++ b/src/controllers/admin/couponController.js @@ -0,0 +1,463 @@ +// const { prisma } = require('../../config/database'); + +// exports.getAllCoupons = async (req, res, next) => { +// try { +// const coupons = await prisma.coupon.findMany({ +// orderBy: { createdAt: 'desc' }, +// }); +// // res.json({ success: true, data: coupons }); +// res.status(200).json({ +// // statusCode: 200, +// status: true, +// message: 'Coupons fetched successfully', +// data: coupons, +// }); +// } catch (error) { +// next(error); +// } +// }; + +// exports.createCoupon = async (req, res, next) => { +// try { +// const { +// code, +// description, +// type, +// value, +// minOrderAmount, +// maxUses, +// validFrom, +// validUntil, +// } = req.body; +// const coupon = await prisma.coupon.create({ +// data: { +// code, +// description, +// type, +// value, +// minOrderAmount, +// maxUses, +// validFrom: new Date(validFrom), +// validUntil: new Date(validUntil), +// }, +// }); + +// res.status(201).json({ +// // statusCode: 201, +// status: true, +// message: 'Coupon created successfully', +// data: coupon, +// }); +// } catch (error) { +// next(error); +// } +// }; + +// controllers/admin/couponController.js - ENHANCED VERSION + +const { prisma } = require('../../config/database'); + +// ========================================== +// ADMIN COUPON MANAGEMENT +// ========================================== + +/** + * @desc Get all coupons (with filters) + * @route GET /api/admin/coupons + * @access Private/Admin + */ +exports.getAllCoupons = async (req, res, next) => { + try { + const { isActive, type, search, page = 1, limit = 20 } = req.query; + + const skip = (parseInt(page) - 1) * parseInt(limit); + + // Build filter + const where = {}; + + if (isActive !== undefined) { + where.isActive = isActive === 'true'; + } + + if (type) { + where.type = type; + } + + if (search) { + where.OR = [ + { code: { contains: search, mode: 'insensitive' } }, + { description: { contains: search, mode: 'insensitive' } }, + ]; + } + + const [coupons, total] = await Promise.all([ + prisma.coupon.findMany({ + where, + orderBy: { createdAt: 'desc' }, + skip, + take: parseInt(limit), + }), + prisma.coupon.count({ where }), + ]); + + // Add usage statistics + const couponsWithStats = coupons.map(coupon => ({ + ...coupon, + usagePercentage: coupon.maxUses + ? Math.round((coupon.usedCount / coupon.maxUses) * 100) + : 0, + isExpired: new Date() > new Date(coupon.validUntil), + isNotStarted: new Date() < new Date(coupon.validFrom), + remainingUses: coupon.maxUses ? coupon.maxUses - coupon.usedCount : null, + })); + + res.status(200).json({ + status: true, + message: 'Coupons fetched successfully', + data: { + coupons: couponsWithStats, + pagination: { + total, + page: parseInt(page), + limit: parseInt(limit), + pages: Math.ceil(total / parseInt(limit)), + }, + }, + }); + } catch (error) { + next(error); + } +}; + +/** + * @desc Get single coupon by ID + * @route GET /api/admin/coupons/:id + * @access Private/Admin + */ +exports.getCouponById = async (req, res, next) => { + try { + const { id } = req.params; + + const coupon = await prisma.coupon.findUnique({ + where: { id }, + }); + + if (!coupon) { + return res.status(404).json({ + status: false, + message: 'Coupon not found', + }); + } + + // Get usage statistics + const usageStats = { + ...coupon, + usagePercentage: coupon.maxUses + ? Math.round((coupon.usedCount / coupon.maxUses) * 100) + : 0, + isExpired: new Date() > new Date(coupon.validUntil), + isNotStarted: new Date() < new Date(coupon.validFrom), + remainingUses: coupon.maxUses ? coupon.maxUses - coupon.usedCount : null, + }; + + res.status(200).json({ + status: true, + message: 'Coupon fetched successfully', + data: usageStats, + }); + } catch (error) { + next(error); + } +}; + +/** + * @desc Create new coupon + * @route POST /api/admin/coupons + * @access Private/Admin + */ +exports.createCoupon = async (req, res, next) => { + try { + const { + code, + description, + type, + value, + minOrderAmount, + maxUses, + validFrom, + validUntil, + isActive = true, + } = req.body; + + // Validate code uniqueness + const existingCoupon = await prisma.coupon.findUnique({ + where: { code: code.toUpperCase() }, + }); + + if (existingCoupon) { + return res.status(400).json({ + status: false, + message: 'Coupon code already exists', + }); + } + + // Validate dates + const fromDate = new Date(validFrom); + const untilDate = new Date(validUntil); + + if (fromDate >= untilDate) { + return res.status(400).json({ + status: false, + message: 'Valid until date must be after valid from date', + }); + } + + // Validate value based on type + if (type === 'PERCENTAGE' && (value < 0 || value > 100)) { + return res.status(400).json({ + status: false, + message: 'Percentage value must be between 0 and 100', + }); + } + + if (type === 'FIXED_AMOUNT' && value < 0) { + return res.status(400).json({ + status: false, + message: 'Fixed amount must be greater than 0', + }); + } + + const coupon = await prisma.coupon.create({ + data: { + code: code.toUpperCase(), + description, + type, + value: parseFloat(value), + minOrderAmount: minOrderAmount ? parseFloat(minOrderAmount) : null, + maxUses: maxUses ? parseInt(maxUses) : null, + validFrom: fromDate, + validUntil: untilDate, + isActive, + }, + }); + + res.status(201).json({ + status: true, + message: 'Coupon created successfully', + data: coupon, + }); + } catch (error) { + console.error('Create coupon error:', error); + next(error); + } +}; + +/** + * @desc Update coupon + * @route PUT /api/admin/coupons/:id + * @access Private/Admin + */ +exports.updateCoupon = async (req, res, next) => { + try { + const { id } = req.params; + const { + code, + description, + type, + value, + minOrderAmount, + maxUses, + validFrom, + validUntil, + isActive, + } = req.body; + + // Check if coupon exists + const existingCoupon = await prisma.coupon.findUnique({ + where: { id }, + }); + + if (!existingCoupon) { + return res.status(404).json({ + status: false, + message: 'Coupon not found', + }); + } + + // If code is being changed, check uniqueness + if (code && code.toUpperCase() !== existingCoupon.code) { + const duplicateCoupon = await prisma.coupon.findUnique({ + where: { code: code.toUpperCase() }, + }); + + if (duplicateCoupon) { + return res.status(400).json({ + status: false, + message: 'Coupon code already exists', + }); + } + } + + // Validate dates if provided + const fromDate = validFrom ? new Date(validFrom) : existingCoupon.validFrom; + const untilDate = validUntil + ? new Date(validUntil) + : existingCoupon.validUntil; + + if (fromDate >= untilDate) { + return res.status(400).json({ + status: false, + message: 'Valid until date must be after valid from date', + }); + } + + const updateData = {}; + if (code) updateData.code = code.toUpperCase(); + if (description !== undefined) updateData.description = description; + if (type) updateData.type = type; + if (value !== undefined) updateData.value = parseFloat(value); + if (minOrderAmount !== undefined) + updateData.minOrderAmount = minOrderAmount + ? parseFloat(minOrderAmount) + : null; + if (maxUses !== undefined) + updateData.maxUses = maxUses ? parseInt(maxUses) : null; + if (validFrom) updateData.validFrom = fromDate; + if (validUntil) updateData.validUntil = untilDate; + if (isActive !== undefined) updateData.isActive = isActive; + + const coupon = await prisma.coupon.update({ + where: { id }, + data: updateData, + }); + + res.status(200).json({ + status: true, + message: 'Coupon updated successfully', + data: coupon, + }); + } catch (error) { + next(error); + } +}; + +/** + * @desc Delete coupon + * @route DELETE /api/admin/coupons/:id + * @access Private/Admin + */ +exports.deleteCoupon = async (req, res, next) => { + try { + const { id } = req.params; + + const coupon = await prisma.coupon.findUnique({ + where: { id }, + }); + + if (!coupon) { + return res.status(404).json({ + status: false, + message: 'Coupon not found', + }); + } + + // Check if coupon has been used + if (coupon.usedCount > 0) { + return res.status(400).json({ + status: false, + message: + 'Cannot delete a coupon that has been used. Consider deactivating it instead.', + }); + } + + await prisma.coupon.delete({ + where: { id }, + }); + + res.status(200).json({ + status: true, + message: 'Coupon deleted successfully', + }); + } catch (error) { + next(error); + } +}; + +/** + * @desc Toggle coupon active status + * @route PATCH /api/admin/coupons/:id/toggle + * @access Private/Admin + */ +exports.toggleCouponStatus = async (req, res, next) => { + try { + const { id } = req.params; + + const coupon = await prisma.coupon.findUnique({ + where: { id }, + }); + + if (!coupon) { + return res.status(404).json({ + status: false, + message: 'Coupon not found', + }); + } + + const updatedCoupon = await prisma.coupon.update({ + where: { id }, + data: { isActive: !coupon.isActive }, + }); + + res.status(200).json({ + status: true, + message: `Coupon ${updatedCoupon.isActive ? 'activated' : 'deactivated'} successfully`, + data: updatedCoupon, + }); + } catch (error) { + next(error); + } +}; + +/** + * @desc Get coupon statistics + * @route GET /api/admin/coupons/stats/overview + * @access Private/Admin + */ +exports.getCouponStats = async (req, res, next) => { + try { + const [totalCoupons, activeCoupons, expiredCoupons, totalRedemptions] = + await Promise.all([ + prisma.coupon.count(), + prisma.coupon.count({ where: { isActive: true } }), + prisma.coupon.count({ + where: { + validUntil: { lt: new Date() }, + }, + }), + prisma.coupon.aggregate({ + _sum: { usedCount: true }, + }), + ]); + + // Get most used coupons + const mostUsed = await prisma.coupon.findMany({ + where: { usedCount: { gt: 0 } }, + orderBy: { usedCount: 'desc' }, + take: 5, + }); + + res.status(200).json({ + status: true, + message: 'Coupon statistics fetched successfully', + data: { + totalCoupons, + activeCoupons, + expiredCoupons, + totalRedemptions: totalRedemptions._sum.usedCount || 0, + mostUsedCoupons: mostUsed, + }, + }); + } catch (error) { + next(error); + } +}; diff --git a/src/controllers/admin/dashboardController.js b/src/controllers/admin/dashboardController.js new file mode 100644 index 0000000..4bb959c --- /dev/null +++ b/src/controllers/admin/dashboardController.js @@ -0,0 +1,589 @@ +const { prisma } = require('../../config/database'); +const Product = require('../../models/mongodb/Product'); +const { + getLowStockProducts, + getInventoryStats, +} = require('../../services/inventoryService'); + +/** + * @desc Get dashboard stats with order overview graphs + * @route GET /api/admin/dashboard/stats + * @access Private/Admin + */ + +exports.getDashboardStats = async (req, res, next) => { + try { + const [ + totalUsers, + totalOrders, + totalProducts, + totalRevenue, + recentOrders, + topSellingProducts, + lowStockProducts, + inventoryStats, + orderOverview, + revenueOverview, + ordersByStatus, + monthlyComparison, + ] = await Promise.all([ + prisma.user.count(), + prisma.order.count(), + Product.countDocuments({ status: 'active' }), + prisma.order.aggregate({ + _sum: { totalAmount: true }, + where: { paymentStatus: 'PAID' }, + }), + prisma.order.findMany({ + take: 5, + orderBy: { createdAt: 'desc' }, + include: { + user: { + select: { id: true, email: true, firstName: true, lastName: true }, + }, + items: true, + }, + }), + getTopSellingProducts(), // โœ… Real top sellers + getLowStockProducts(10), // โœ… Low stock alerts + getInventoryStats(), // โœ… Inventory overview + getOrderOverview(), + getRevenueOverview(), + getOrdersByStatus(), + getMonthlyComparison(), + ]); + + // Enhance recent orders with product images + const orderProductIds = recentOrders + .flatMap(order => order.items.map(item => item.productId)) + .filter(Boolean); + + const orderProducts = await Product.find({ + _id: { $in: orderProductIds }, + }).lean(); + + const productMap = {}; + orderProducts.forEach(product => { + productMap[product._id.toString()] = { + ...product, + displayImage: getProductImage(product), + }; + }); + + const enhancedRecentOrders = recentOrders.map(order => ({ + ...order, + items: order.items.map(item => ({ + ...item, + productImage: + productMap[item.productId]?.displayImage || + 'https://via.placeholder.com/300', + productDetails: productMap[item.productId] || null, + })), + })); + + res.status(200).json({ + statusCode: 200, + status: true, + message: 'Dashboard stats fetched successfully', + data: { + // Summary Stats + totalUsers, + totalOrders, + totalProducts, + totalRevenue: parseFloat(totalRevenue._sum.totalAmount || 0), + + // โœ… Inventory Stats + inventory: inventoryStats, + + // Lists + recentOrders: enhancedRecentOrders, + topProducts: topSellingProducts, // โœ… Real selling data + lowStockProducts, // โœ… Products needing restock + + // Charts + charts: { + orderOverview, + revenueOverview, + ordersByStatus, + monthlyComparison, + }, + }, + }); + } catch (error) { + console.error('Dashboard stats error:', error); + next(error); + } +}; + +// โœ… GET REAL TOP SELLING PRODUCTS +async function getTopSellingProducts() { + try { + // Get sales data from order items + const salesData = await prisma.orderItem.groupBy({ + by: ['productId'], + _sum: { quantity: true }, + _count: { productId: true }, + orderBy: { _sum: { quantity: 'desc' } }, + take: 10, + }); + + if (salesData.length === 0) { + // Fallback: Return recent products + const fallback = await Product.find({ status: 'active' }) + .sort({ createdAt: -1 }) + .limit(5) + .lean(); + + return fallback.map(p => ({ + ...p, + _id: p._id.toString(), + displayImage: getProductImage(p), + totalSold: 0, + totalOrders: 0, + stock: p.stock || 0, + stockStatus: getStockStatus(p.stock || 0), + })); + } + + const productIds = salesData.map(item => item.productId); + const products = await Product.find({ + _id: { $in: productIds }, + status: 'active', + }).lean(); + + const statsMap = {}; + salesData.forEach(item => { + statsMap[item.productId] = { + totalSold: item._sum.quantity || 0, + totalOrders: item._count.productId || 0, + }; + }); + + function calculateStock(product) { + if (product.variants?.length > 0) { + return product.variants + .filter(v => v.isActive) + .reduce((sum, v) => sum + (v.inventory?.quantity || 0), 0); + } + return product.stock || 0; + } + + const topProducts = products + .map(product => { + const stats = statsMap[product._id.toString()] || { + totalSold: 0, + totalOrders: 0, + }; + + const stock = + product.variants?.length > 0 + ? product.variants + .filter(v => v.isActive) + .reduce((sum, v) => sum + (v.inventory?.quantity || 0), 0) + : product.stock || 0; + + return { + _id: product._id.toString(), + name: product.name, + slug: product.slug, + basePrice: product.basePrice, + displayImage: getProductImage(product), + totalSold: stats.totalSold, + totalOrders: stats.totalOrders, + revenue: stats.totalSold * (product.basePrice || 0), + stock, + stockStatus: getStockStatus(stock), + }; + }) + .sort((a, b) => b.totalSold - a.totalSold) + .slice(0, 5); + + return topProducts; + } catch (error) { + console.error('Error fetching top selling products:', error); + return []; + } +} + +function getStockStatus(stock) { + if (stock === 0) return 'OUT_OF_STOCK'; + if (stock <= 5) return 'CRITICAL'; + if (stock <= 10) return 'LOW'; + return 'IN_STOCK'; +} + +function getProductImage(product) { + return ( + product.images?.gallery?.[0] || + product.images?.primary || + product.variants?.[0]?.images?.[0] || + 'https://via.placeholder.com/300' + ); +} + +/** + * Get daily order count for last 30 days + */ +// async function getOrderOverview() { +// const thirtyDaysAgo = new Date(); +// thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30); + +// // Group orders by date +// const orders = await prisma.order.findMany({ +// where: { +// createdAt: { +// gte: thirtyDaysAgo, +// }, +// }, +// select: { +// createdAt: true, +// status: true, +// }, +// }); + +// // Create date map for last 30 days +// const dateMap = {}; +// for (let i = 29; i >= 0; i--) { +// const date = new Date(); +// date.setDate(date.getDate() - i); +// const dateKey = date.toISOString().split('T')[0]; +// dateMap[dateKey] = { total: 0, completed: 0, pending: 0, cancelled: 0 }; +// } + +// // Count orders by date +// orders.forEach(order => { +// const dateKey = order.createdAt.toISOString().split('T')[0]; +// if (dateMap[dateKey]) { +// dateMap[dateKey].total++; + +// if (order.status === 'DELIVERED') { +// dateMap[dateKey].completed++; +// } else if (['PENDING', 'CONFIRMED', 'PROCESSING', 'SHIPPED'].includes(order.status)) { +// dateMap[dateKey].pending++; +// } else if (order.status === 'CANCELLED') { +// dateMap[dateKey].cancelled++; +// } +// } +// }); + +// // Convert to array format for charts +// return Object.entries(dateMap).map(([date, counts]) => ({ +// date, +// label: new Date(date).toLocaleDateString('en-IN', { month: 'short', day: 'numeric' }), +// total: counts.total, +// completed: counts.completed, +// pending: counts.pending, +// cancelled: counts.cancelled, +// })); +// } + +async function getOrderOverview() { + const thirtyDaysAgo = new Date(); + thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30); + + const orders = await prisma.order.findMany({ + where: { createdAt: { gte: thirtyDaysAgo } }, + select: { createdAt: true, status: true }, + }); + + const dateMap = {}; + for (let i = 29; i >= 0; i--) { + const date = new Date(); + date.setDate(date.getDate() - i); + const dateKey = date.toISOString().split('T')[0]; + dateMap[dateKey] = { total: 0, completed: 0, pending: 0, cancelled: 0 }; + } + + orders.forEach(order => { + const dateKey = order.createdAt.toISOString().split('T')[0]; + if (dateMap[dateKey]) { + dateMap[dateKey].total++; + if (order.status === 'DELIVERED') dateMap[dateKey].completed++; + else if ( + ['PENDING', 'CONFIRMED', 'PROCESSING', 'SHIPPED'].includes(order.status) + ) + dateMap[dateKey].pending++; + else if (order.status === 'CANCELLED') dateMap[dateKey].cancelled++; + } + }); + + return Object.entries(dateMap).map(([date, counts]) => ({ + date, + label: new Date(date).toLocaleDateString('en-IN', { + month: 'short', + day: 'numeric', + }), + ...counts, + })); +} + +/** + * Get daily revenue for last 30 days + */ +// async function getRevenueOverview() { +// const thirtyDaysAgo = new Date(); +// thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30); + +// const orders = await prisma.order.findMany({ +// where: { +// createdAt: { +// gte: thirtyDaysAgo, +// }, +// paymentStatus: 'PAID', +// }, +// select: { +// createdAt: true, +// totalAmount: true, +// }, +// }); + +// // Create date map +// const dateMap = {}; +// for (let i = 29; i >= 0; i--) { +// const date = new Date(); +// date.setDate(date.getDate() - i); +// const dateKey = date.toISOString().split('T')[0]; +// dateMap[dateKey] = 0; +// } + +// // Sum revenue by date +// orders.forEach(order => { +// const dateKey = order.createdAt.toISOString().split('T')[0]; +// if (dateMap[dateKey] !== undefined) { +// dateMap[dateKey] += parseFloat(order.totalAmount); +// } +// }); + +// return Object.entries(dateMap).map(([date, revenue]) => ({ +// date, +// label: new Date(date).toLocaleDateString('en-IN', { month: 'short', day: 'numeric' }), +// revenue: Math.round(revenue * 100) / 100, // Round to 2 decimals +// })); +// } + +async function getRevenueOverview() { + const thirtyDaysAgo = new Date(); + thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30); + + const orders = await prisma.order.findMany({ + where: { createdAt: { gte: thirtyDaysAgo }, paymentStatus: 'PAID' }, + select: { createdAt: true, totalAmount: true }, + }); + + const dateMap = {}; + for (let i = 29; i >= 0; i--) { + const date = new Date(); + date.setDate(date.getDate() - i); + dateMap[date.toISOString().split('T')[0]] = 0; + } + + orders.forEach(order => { + const dateKey = order.createdAt.toISOString().split('T')[0]; + if (dateMap[dateKey] !== undefined) { + dateMap[dateKey] += parseFloat(order.totalAmount); + } + }); + + return Object.entries(dateMap).map(([date, revenue]) => ({ + date, + label: new Date(date).toLocaleDateString('en-IN', { + month: 'short', + day: 'numeric', + }), + revenue: Math.round(revenue * 100) / 100, + })); +} + +/** + * Get order counts by status + */ +// async function getOrdersByStatus() { +// const statusCounts = await prisma.order.groupBy({ +// by: ['status'], +// _count: true, +// }); + +// const statusLabels = { +// PENDING: 'Pending', +// CONFIRMED: 'Confirmed', +// PROCESSING: 'Processing', +// SHIPPED: 'Shipped', +// DELIVERED: 'Delivered', +// CANCELLED: 'Cancelled', +// RETURN_REQUESTED: 'Return Requested', +// }; + +// const statusColors = { +// PENDING: '#FCD34D', +// CONFIRMED: '#60A5FA', +// PROCESSING: '#A78BFA', +// SHIPPED: '#C084FC', +// DELIVERED: '#34D399', +// CANCELLED: '#F87171', +// RETURN_REQUESTED: '#FB923C', +// }; + +// return statusCounts.map(item => ({ +// status: item.status, +// label: statusLabels[item.status] || item.status, +// count: item._count, +// color: statusColors[item.status] || '#9CA3AF', +// })); +// } + +async function getOrdersByStatus() { + const statusCounts = await prisma.order.groupBy({ + by: ['status'], + _count: true, + }); + + const labels = { + PENDING: 'Pending', + CONFIRMED: 'Confirmed', + PROCESSING: 'Processing', + SHIPPED: 'Shipped', + DELIVERED: 'Delivered', + CANCELLED: 'Cancelled', + RETURN_REQUESTED: 'Return Requested', + }; + + const colors = { + PENDING: '#FCD34D', + CONFIRMED: '#60A5FA', + PROCESSING: '#A78BFA', + SHIPPED: '#C084FC', + DELIVERED: '#34D399', + CANCELLED: '#F87171', + RETURN_REQUESTED: '#FB923C', + }; + + return statusCounts.map(item => ({ + status: item.status, + label: labels[item.status] || item.status, + count: item._count, + color: colors[item.status] || '#9CA3AF', + })); +} + +/** + * Compare current month vs previous month + */ +// async function getMonthlyComparison() { +// const now = new Date(); + +// // Current month dates +// const currentMonthStart = new Date(now.getFullYear(), now.getMonth(), 1); +// const currentMonthEnd = new Date(now.getFullYear(), now.getMonth() + 1, 0); + +// // Previous month dates +// const previousMonthStart = new Date(now.getFullYear(), now.getMonth() - 1, 1); +// const previousMonthEnd = new Date(now.getFullYear(), now.getMonth(), 0); + +// const [currentMonth, previousMonth] = await Promise.all([ +// prisma.order.aggregate({ +// where: { +// createdAt: { +// gte: currentMonthStart, +// lte: currentMonthEnd, +// }, +// }, +// _count: true, +// _sum: { +// totalAmount: true, +// }, +// }), +// prisma.order.aggregate({ +// where: { +// createdAt: { +// gte: previousMonthStart, +// lte: previousMonthEnd, +// }, +// }, +// _count: true, +// _sum: { +// totalAmount: true, +// }, +// }), +// ]); + +// const currentRevenue = parseFloat(currentMonth._sum.totalAmount || 0); +// const previousRevenue = parseFloat(previousMonth._sum.totalAmount || 0); + +// const orderGrowth = previousMonth._count > 0 +// ? ((currentMonth._count - previousMonth._count) / previousMonth._count) * 100 +// : 100; + +// const revenueGrowth = previousRevenue > 0 +// ? ((currentRevenue - previousRevenue) / previousRevenue) * 100 +// : 100; + +// return { +// currentMonth: { +// orders: currentMonth._count, +// revenue: Math.round(currentRevenue * 100) / 100, +// label: currentMonthStart.toLocaleDateString('en-IN', { month: 'long' }), +// }, +// previousMonth: { +// orders: previousMonth._count, +// revenue: Math.round(previousRevenue * 100) / 100, +// label: previousMonthStart.toLocaleDateString('en-IN', { month: 'long' }), +// }, +// growth: { +// orders: Math.round(orderGrowth * 10) / 10, +// revenue: Math.round(revenueGrowth * 10) / 10, +// }, +// }; +// } + +async function getMonthlyComparison() { + const now = new Date(); + const currentMonthStart = new Date(now.getFullYear(), now.getMonth(), 1); + const currentMonthEnd = new Date(now.getFullYear(), now.getMonth() + 1, 0); + const previousMonthStart = new Date(now.getFullYear(), now.getMonth() - 1, 1); + const previousMonthEnd = new Date(now.getFullYear(), now.getMonth(), 0); + + const [currentMonth, previousMonth] = await Promise.all([ + prisma.order.aggregate({ + where: { createdAt: { gte: currentMonthStart, lte: currentMonthEnd } }, + _count: true, + _sum: { totalAmount: true }, + }), + prisma.order.aggregate({ + where: { createdAt: { gte: previousMonthStart, lte: previousMonthEnd } }, + _count: true, + _sum: { totalAmount: true }, + }), + ]); + + const currentRevenue = parseFloat(currentMonth._sum.totalAmount || 0); + const previousRevenue = parseFloat(previousMonth._sum.totalAmount || 0); + + return { + currentMonth: { + orders: currentMonth._count, + revenue: Math.round(currentRevenue * 100) / 100, + label: currentMonthStart.toLocaleDateString('en-IN', { month: 'long' }), + }, + previousMonth: { + orders: previousMonth._count, + revenue: Math.round(previousRevenue * 100) / 100, + label: previousMonthStart.toLocaleDateString('en-IN', { month: 'long' }), + }, + growth: { + orders: + previousMonth._count > 0 + ? Math.round( + ((currentMonth._count - previousMonth._count) / + previousMonth._count) * + 1000 + ) / 10 + : 100, + revenue: + previousRevenue > 0 + ? Math.round( + ((currentRevenue - previousRevenue) / previousRevenue) * 1000 + ) / 10 + : 100, + }, + }; +} diff --git a/src/controllers/admin/orderController.js b/src/controllers/admin/orderController.js new file mode 100644 index 0000000..01f632d --- /dev/null +++ b/src/controllers/admin/orderController.js @@ -0,0 +1,395 @@ +const { prisma } = require('../../config/database'); + +// exports.getAllOrders = async (req, res, next) => { +// try { +// const { page = 1, limit = 20, status, paymentStatus, userId } = req.query; +// const skip = (page - 1) * limit; + +// const where = {}; +// if (status) where.status = status; +// if (paymentStatus) where.paymentStatus = paymentStatus; +// if (userId) where.userId = userId; + +// const [orders, total] = await Promise.all([ +// prisma.order.findMany({ +// where, +// include: { +// items: true, +// address: true, +// user: { +// select: { id: true, email: true, firstName: true, lastName: true }, +// }, +// }, +// orderBy: { createdAt: 'desc' }, +// skip: +skip, +// take: +limit, +// }), +// prisma.order.count({ where }), +// ]); + +// // res.json({ +// // success: true, +// // data: { orders, pagination: { page: +page, limit: +limit, total, pages: Math.ceil(total / limit) } }, +// // }); +// res.status(200).json({ +// statusCode: 200, +// status: true, +// message: 'Orders fetched successfully', +// data: { +// orders, +// pagination: { +// page: +page, +// limit: +limit, +// total, +// pages: Math.ceil(total / limit), +// }, +// }, +// }); +// } catch (error) { +// next(error); +// } +// }; + +// exports.getOrderDetails = async (req, res, next) => { +// try { +// const { id } = req.params; + +// const order = await prisma.order.findUnique({ +// where: { id }, +// include: { +// items: { +// include: { +// product: true, // if you want product details +// } +// }, +// address: true, +// user: { +// select: { +// id: true, +// firstName: true, +// lastName: true, +// email: true, +// phone: true, +// }, +// }, +// }, +// }); + +// if (!order) { +// return res.status(404).json({ +// success: false, +// message: "Order not found", +// }); +// } + +// res.json({ +// success: true, +// data: order, +// }); +// } catch (error) { +// next(error); +// } +// }; + + + + +exports.getAllOrders = async (req, res, next) => { + try { + const { + page = 1, + limit = 20, + status, + search, + sortBy = 'createdAt', + sortOrder = 'desc', + } = req.query; + + const skip = (parseInt(page) - 1) * parseInt(limit); + + const where = {}; + + if (status) { + where.status = status; + } + + if (search) { + where.OR = [ + { orderNumber: { contains: search, mode: 'insensitive' } }, + { trackingNumber: { contains: search, mode: 'insensitive' } }, + ]; + } + + const [orders, total] = await Promise.all([ + prisma.order.findMany({ + where, + include: { + user: { + select: { + id: true, + email: true, + firstName: true, + lastName: true, + phone: true, + }, + }, + address: true, + items: true, + // โœ… Include latest status change + statusHistory: { + take: 1, + orderBy: { createdAt: 'desc' }, + include: { + admin: { + select: { + firstName: true, + lastName: true, + }, + }, + }, + }, + }, + orderBy: { + [sortBy]: sortOrder, + }, + skip, + take: parseInt(limit), + }), + prisma.order.count({ where }), + ]); + + res.status(200).json({ + success: true, + data: { + orders, + pagination: { + total, + page: parseInt(page), + limit: parseInt(limit), + pages: Math.ceil(total / parseInt(limit)), + }, + }, + }); + } catch (error) { + console.error('โŒ Get all orders error:', error); + next(error); + } +}; + + +// ** +// * @desc Get single order with full history +// * @route GET /api/admin/orders/:orderId +// * @access Private/Admin +// */ +exports.getOrderById = async (req, res, next) => { + try { + const { orderId } = req.params; + + const order = await prisma.order.findUnique({ + where: { id: orderId }, + include: { + user: { + select: { + id: true, + email: true, + firstName: true, + lastName: true, + phone: true, + }, + }, + address: true, + items: true, + // โœ… Include full status history + statusHistory: { + orderBy: { createdAt: 'desc' }, + include: { + admin: { + select: { + id: true, + email: true, + firstName: true, + lastName: true, + }, + }, + }, + }, + }, + }); + + if (!order) { + return res.status(404).json({ + success: false, + message: 'Order not found', + }); + } + + res.status(200).json({ + success: true, + data: order, + }); + } catch (error) { + console.error('โŒ Get order error:', error); + next(error); + } +}; + + +/** + * @desc Get order status history + * @route GET /api/admin/orders/:orderId/history + * @access Private/Admin + */ +exports.getOrderStatusHistory = async (req, res, next) => { + try { + const { orderId } = req.params; + + const history = await prisma.orderStatusHistory.findMany({ + where: { orderId }, + include: { + admin: { + select: { + id: true, + email: true, + firstName: true, + lastName: true, + }, + }, + }, + orderBy: { createdAt: 'desc' }, + }); + + res.status(200).json({ + success: true, + data: history, + }); + } catch (error) { + console.error('โŒ Get status history error:', error); + next(error); + } +}; + + + + +/** + * @desc Get status change statistics + * @route GET /api/admin/orders/stats/status-changes + * @access Private/Admin + */ +exports.getStatusChangeStats = async (req, res, next) => { + try { + const { startDate, endDate } = req.query; + + const where = {}; + + if (startDate || endDate) { + where.createdAt = {}; + if (startDate) where.createdAt.gte = new Date(startDate); + if (endDate) where.createdAt.lte = new Date(endDate); + } + + // Count status changes by status + const statusChanges = await prisma.orderStatusHistory.groupBy({ + by: ['toStatus'], + where, + _count: true, + }); + + // Count status changes by admin + const changesByAdmin = await prisma.orderStatusHistory.groupBy({ + by: ['changedBy'], + where, + _count: true, + orderBy: { + _count: { + changedBy: 'desc', + }, + }, + take: 10, + }); + + // Get admin details + const adminIds = changesByAdmin + .map(item => item.changedBy) + .filter(Boolean); + + const admins = await prisma.user.findMany({ + where: { id: { in: adminIds } }, + select: { + id: true, + firstName: true, + lastName: true, + email: true, + }, + }); + + const adminMap = Object.fromEntries( + admins.map(admin => [admin.id, admin]) + ); + + const changesByAdminWithDetails = changesByAdmin.map(item => ({ + admin: item.changedBy ? adminMap[item.changedBy] : null, + count: item._count, + })); + + res.status(200).json({ + success: true, + data: { + statusChanges, + changesByAdmin: changesByAdminWithDetails, + }, + }); + } catch (error) { + console.error('โŒ Get status stats error:', error); + next(error); + } +}; + + + +exports.getOrderDetails = async (req, res, next) => { + try { + const { id } = req.params; + + const order = await prisma.order.findUnique({ + where: { id }, + include: { + items: true, // OrderItem snapshot data + address: true, // shipping address + user: { + select: { + id: true, + firstName: true, + lastName: true, + email: true, + phone: true, + }, + }, + }, + }); + + if (!order) { + return res.status(404).json({ + // success: false, + // message: "Order not found", + statusCode: 404, + status: false, + message: 'Order not found', + }); + } + + // res.json({ + // success: true, + // data: order, + // }); + res.status(200).json({ + statusCode: 200, + status: true, + message: 'Order details fetched successfully', + data: order, + }); + } catch (error) { + next(error); + } +}; diff --git a/src/controllers/admin/productController.js b/src/controllers/admin/productController.js new file mode 100644 index 0000000..11276b0 --- /dev/null +++ b/src/controllers/admin/productController.js @@ -0,0 +1,782 @@ +const Product = require('../../models/mongodb/Product'); +const uploadToS3 = require("../../utils/uploadToS3"); + + +// exports.getAllProducts = async (req, res, next) => { +// try { +// const { page = 1, limit = 20, status, category, search } = req.query; +// const skip = (page - 1) * limit; + +// const query = {}; +// if (status) query.status = status; +// if (category) query.category = category; +// if (search) query.$text = { $search: search }; + +// const [products, total] = await Promise.all([ +// Product.find(query) +// .sort({ createdAt: -1 }) +// .skip(skip) +// .limit(+limit) +// .lean(), +// Product.countDocuments(query), +// ]); + +// // res.json({ +// // success: true, +// // data: { products, pagination: { page: +page, limit: +limit, total, pages: Math.ceil(total / limit) } }, +// // }); +// return res.status(200).json({ +// statusCode: 200, +// status: true, +// message: 'Products fetched successfully', +// data: { +// products, +// pagination: { +// page: +page, +// limit: +limit, +// total, +// pages: Math.ceil(total / limit), +// }, +// }, +// }); +// } catch (error) { +// next(error); +// } +// }; + +// exports.createProduct = async (req, res, next) => { +// try { +// const product = new Product(req.body); +// await product.save(); +// res.status(201).json({ success: true, message: 'Product created', data: product }); +// } catch (error) { +// next(error); +// } +// }; + +// exports.createProduct = async (req, res, next) => { +// try { +// let { name, slug, ...rest } = req.body; + +// // Generate slug from name if not provided +// if (!slug) { +// slug = name +// .toLowerCase() +// .replace(/[^a-z0-9]+/g, '-') // replace spaces & special chars +// .replace(/(^-|-$)/g, ''); // remove leading/trailing hyphens +// } + +// // Ensure slug is unique +// let slugExists = await Product.findOne({ slug }); +// let counter = 1; +// const baseSlug = slug; +// while (slugExists) { +// slug = `${baseSlug}-${counter}`; +// slugExists = await Product.findOne({ slug }); +// counter++; +// } + +// // โœ… Upload images manually +// const primaryImage = req.files?.primaryImage +// ? await uploadToS3(req.files.primaryImage[0]) +// : null; + +// const galleryImages = req.files?.galleryImages +// ? await Promise.all( +// req.files.galleryImages.map((file) => uploadToS3(file)) +// ) +// : []; + +// // const product = new Product({ name, slug, ...rest }); +// const product = new Product({ +// name, +// slug, +// ...rest, +// images: { +// primary: primaryImage, +// gallery: galleryImages, +// }, +// }); +// await product.save(); + +// // res.status(201).json({ success: true, message: 'Product created', data: product }); +// return res.status(201).json({ +// // statusCode: 201, +// status: true, +// message: 'Product created successfully', +// data: product, +// }); +// } catch (error) { +// next(error); +// } +// }; + + + +// exports.createProduct = async (req, res, next) => { +// try { +// let { name, hasVariants, variants, ...rest } = req.body; +// let slug; +// // slug generation +// if (!slug) { +// slug = name +// .toLowerCase() +// .replace(/[^a-z0-9]+/g, "-") +// .replace(/(^-|-$)/g, ""); +// } + +// let exists = await Product.findOne({ slug }); +// let i = 1; +// while (exists) { +// slug = `${slug}-${i++}`; +// exists = await Product.findOne({ slug }); +// } + +// // Upload main images +// const primaryImage = req.files?.primaryImage +// ? await uploadToS3(req.files.primaryImage[0]) +// : null; + +// const galleryImages = req.files?.galleryImages +// ? await Promise.all( +// req.files.galleryImages.map((f) => uploadToS3(f)) +// ) +// : []; + +// const productData = { +// name, +// slug, +// ...rest, +// hasVariants: hasVariants === "true", +// }; + +// // ๐Ÿ”ฅ VARIANT LOGIC +// if (hasVariants === "true") { +// const parsedVariants = JSON.parse(variants); // IMPORTANT + +// productData.variants = await Promise.all( +// parsedVariants.map(async (variant) => { +// const variantImages = +// req.files?.[`variantImages_${variant.color}`] +// ? await Promise.all( +// req.files[`variantImages_${variant.color}`].map(uploadToS3) +// ) +// : []; + +// return { +// size: variant.size, +// color: variant.color, +// sku: variant.sku, +// price: Number(variant.price), +// compareAtPrice: Number(variant.compareAtPrice), +// inventory: { +// quantity: Number(variant.quantity), +// trackInventory: true, +// }, +// images: variantImages, +// }; +// }) +// ); +// } else { +// // simple product images +// productData.images = { +// primary: primaryImage, +// gallery: galleryImages, +// }; +// } + +// const product = await Product.create(productData); + +// res.status(201).json({ +// status: true, +// message: "Product created successfully", +// data: product, +// }); +// } catch (err) { +// next(err); +// } +// }; + + +// exports.createProduct = async (req, res, next) => { +// try { +// const { name, hasVariants, variants, ...rest } = req.body; + +// // โœ… Validate name +// if (!name || name.trim() === "") { +// return res.status(400).json({ +// status: false, +// message: "Product name is required", +// }); +// } + +// // slug generation +// let slug = name +// .toLowerCase() +// .replace(/[^a-z0-9]+/g, "-") +// .replace(/(^-|-$)/g, ""); + +// // ensure unique slug +// let exists = await Product.findOne({ slug }); +// let i = 1; +// while (exists) { +// slug = `${slug}-${i++}`; +// exists = await Product.findOne({ slug }); +// } + +// // Upload main images +// const primaryImage = req.files?.primaryImage +// ? await uploadToS3(req.files.primaryImage[0]) +// : null; + +// const galleryImages = req.files?.galleryImages +// ? await Promise.all(req.files.galleryImages.map(uploadToS3)) +// : []; + +// const productData = { +// name, +// slug, +// ...rest, +// hasVariants: hasVariants === "true", +// }; + +// // ๐Ÿ”ฅ VARIANT LOGIC +// if (hasVariants === "true") { +// const parsedVariants = JSON.parse(variants); // IMPORTANT + +// productData.variants = await Promise.all( +// parsedVariants.map(async (variant) => { +// const variantImages = +// req.files?.[`variantImages_${variant.color}`] +// ? await Promise.all( +// req.files[`variantImages_${variant.color}`].map(uploadToS3) +// ) +// : []; + +// return { +// size: variant.size, +// color: variant.color, +// sku: variant.sku, +// price: Number(variant.price), +// compareAtPrice: Number(variant.compareAtPrice), +// inventory: { +// quantity: Number(variant.quantity), +// trackInventory: true, +// }, +// images: variantImages, +// }; +// }) +// ); +// } else { +// // simple product images +// productData.images = { +// primary: primaryImage, +// gallery: galleryImages, +// }; +// } + +// const product = await Product.create(productData); + +// res.status(201).json({ +// status: true, +// message: "Product created successfully", +// data: product, +// }); +// } catch (err) { +// next(err); +// } +// }; + + + +// exports.createProduct = async (req, res, next) => { +// try { +// const { name, hasVariants, variants, ...rest } = req.body; + +// console.log('๐Ÿ“ฅ Request body:', { name, hasVariants, variantsCount: variants ? 'YES' : 'NO' }); +// console.log('๐Ÿ“ฅ Files received:', req.files ? req.files.length : 0); + +// // โœ… Validate name +// if (!name || name.trim() === "") { +// return res.status(400).json({ +// status: false, +// message: "Product name is required", +// }); +// } + +// // Generate unique slug +// let slug = name +// .toLowerCase() +// .replace(/[^a-z0-9]+/g, "-") +// .replace(/(^-|-$)/g, ""); + +// let exists = await Product.findOne({ slug }); +// let i = 1; +// while (exists) { +// slug = `${slug}-${i++}`; +// exists = await Product.findOne({ slug }); +// } + +// const productData = { +// name, +// slug, +// ...rest, +// hasVariants: hasVariants === "true" || hasVariants === true, +// }; + +// // ====================== +// // VARIANT MODE +// // ====================== +// if (productData.hasVariants) { +// console.log('๐ŸŽจ Processing variant product...'); + +// if (!variants) { +// return res.status(400).json({ +// status: false, +// message: "Variants data is required when hasVariants is true", +// }); +// } + +// const parsedVariants = JSON.parse(variants); +// console.log('๐Ÿ“ฆ Parsed variants:', parsedVariants.length); + +// // โœ… Convert req.files array to object grouped by fieldname +// const filesGrouped = {}; +// if (req.files && req.files.length > 0) { +// req.files.forEach(file => { +// if (!filesGrouped[file.fieldname]) { +// filesGrouped[file.fieldname] = []; +// } +// filesGrouped[file.fieldname].push(file); +// }); +// } + +// console.log('๐Ÿ“ธ Files grouped:', Object.keys(filesGrouped)); + +// // Process each variant +// productData.variants = await Promise.all( +// parsedVariants.map(async (variant) => { +// const color = variant.color; +// const fieldName = `variantImages_${color}`; + +// console.log(`๐Ÿ” Looking for images with fieldname: ${fieldName}`); + +// // Get images for this variant +// const variantFiles = filesGrouped[fieldName] || []; +// console.log(`๐Ÿ“ธ Found ${variantFiles.length} images for ${color}`); + +// // Upload images to S3 +// const variantImages = variantFiles.length > 0 +// ? await Promise.all(variantFiles.map(uploadToS3)) +// : []; + +// console.log(`โœ… Uploaded ${variantImages.length} images for ${color}`); + +// return { +// size: variant.size || 'default', +// color: variant.color, +// sku: variant.sku, +// price: Number(variant.price), +// compareAtPrice: variant.compareAtPrice ? Number(variant.compareAtPrice) : null, +// inventory: { +// quantity: Number(variant.quantity || variant.stock || 0), +// trackInventory: true, +// }, +// images: variantImages, +// isActive: true, +// }; +// }) +// ); + +// console.log('โœ… All variants processed:', productData.variants.length); +// } +// // ====================== +// // SIMPLE PRODUCT MODE +// // ====================== +// else { +// console.log('๐Ÿ“ฆ Processing simple product...'); + +// // โœ… Handle files from req.files array +// let primaryImage = null; +// let galleryImages = []; + +// if (req.files && req.files.length > 0) { +// // Group files by fieldname +// const filesGrouped = {}; +// req.files.forEach(file => { +// if (!filesGrouped[file.fieldname]) { +// filesGrouped[file.fieldname] = []; +// } +// filesGrouped[file.fieldname].push(file); +// }); + +// // Upload primary image +// if (filesGrouped['primaryImage'] && filesGrouped['primaryImage'][0]) { +// primaryImage = await uploadToS3(filesGrouped['primaryImage'][0]); +// } + +// // Upload gallery images +// if (filesGrouped['galleryImages']) { +// galleryImages = await Promise.all( +// filesGrouped['galleryImages'].map(uploadToS3) +// ); +// } +// } + +// productData.images = { +// primary: primaryImage, +// gallery: galleryImages, +// videos: [], +// }; + +// console.log('โœ… Images uploaded:', { +// primary: !!primaryImage, +// gallery: galleryImages.length, +// }); +// } + +// // Create product in MongoDB +// const product = await Product.create(productData); + +// console.log('โœ… Product created:', product._id); + +// res.status(201).json({ +// status: true, +// message: "Product created successfully", +// data: product, +// }); +// } catch (err) { +// console.error('โŒ Error creating product:', err); + +// // Send detailed error for debugging +// if (process.env.NODE_ENV === 'development') { +// return res.status(400).json({ +// status: false, +// message: "Failed to create product", +// error: err.message, +// stack: err.stack, +// }); +// } + +// next(err); +// } +// }; + + +exports.getAllProducts = async (req, res, next) => { + try { + const { page = 1, limit = 20, status, category, search } = req.query; + const skip = (page - 1) * limit; + + const query = {}; + if (status) query.status = status; + if (category) query.category = category; + if (search) query.$text = { $search: search }; + + const [products, total] = await Promise.all([ + Product.find(query) + .sort({ createdAt: -1 }) + .skip(skip) + .limit(+limit) + .lean(), + Product.countDocuments(query), + ]); + + // โœ… Compute real-time stock for each product + const productsWithStock = products.map(product => { + const stockInfo = computeStockInfo(product); + return { + ...product, + ...stockInfo, + }; + }); + + return res.status(200).json({ + statusCode: 200, + status: true, + message: 'Products fetched successfully', + data: { + products: productsWithStock, + pagination: { + page: +page, + limit: +limit, + total, + pages: Math.ceil(total / limit), + }, + }, + }); + } catch (error) { + next(error); + } +}; + + +// โœ… Central stock calculator โ€” use this everywhere +function computeStockInfo(product) { + let totalStock = 0; + let variantStockDetails = []; + + if (product.hasVariants && product.variants?.length > 0) { + // Sum stock across all active variants + variantStockDetails = product.variants + .filter(v => v.isActive) + .map(v => ({ + variantId: v._id?.toString(), + sku: v.sku, + size: v.size, + color: v.color, + price: v.price, + stock: v.inventory?.quantity || 0, + trackInventory: v.inventory?.trackInventory ?? true, + stockStatus: getStockStatus(v.inventory?.quantity || 0), + })); + + totalStock = variantStockDetails.reduce((sum, v) => sum + v.stock, 0); + } else { + // Non-variant product โ€” use root stock field + totalStock = product.stock || 0; + variantStockDetails = []; + } + + return { + totalStock, + stockStatus: getStockStatus(totalStock), + variantStock: variantStockDetails, // per-variant breakdown + }; +} + +function getStockStatus(stock) { + if (stock === 0) return 'OUT_OF_STOCK'; + if (stock <= 5) return 'CRITICAL'; + if (stock <= 10) return 'LOW'; + return 'IN_STOCK'; +} + + +exports.createProduct = async (req, res, next) => { + try { + const { name, hasVariants, variants, ...rest } = req.body; + + console.log('๐Ÿ“ฅ Request body:', { name, hasVariants, variantsCount: variants ? 'YES' : 'NO' }); + console.log('๐Ÿ“ฅ Files received:', req.files ? req.files.length : 0); + + // โœ… Validate name + if (!name || name.trim() === "") { + return res.status(400).json({ + status: false, + message: "Product name is required", + }); + } + + // Generate unique slug + let slug = name + .toLowerCase() + .replace(/[^a-z0-9]+/g, "-") + .replace(/(^-|-$)/g, ""); + + let exists = await Product.findOne({ slug }); + let i = 1; + while (exists) { + slug = `${slug}-${i++}`; + exists = await Product.findOne({ slug }); + } + + // โœ… Build productData carefully to avoid empty slug + const productData = { + name, + slug, // Generated slug + hasVariants: hasVariants === "true" || hasVariants === true, + }; + + // โœ… Add other fields from rest, but skip 'slug' if it exists + Object.keys(rest).forEach(key => { + if (key !== 'slug') { + productData[key] = rest[key]; + } + }); + + // ====================== + // VARIANT MODE + // ====================== + if (productData.hasVariants) { + console.log('๐ŸŽจ Processing variant product...'); + + if (!variants) { + return res.status(400).json({ + status: false, + message: "Variants data is required when hasVariants is true", + }); + } + + const parsedVariants = JSON.parse(variants); + console.log('๐Ÿ“ฆ Parsed variants:', parsedVariants.length); + + // โœ… Convert req.files array to object grouped by fieldname + const filesGrouped = {}; + if (req.files && req.files.length > 0) { + req.files.forEach(file => { + if (!filesGrouped[file.fieldname]) { + filesGrouped[file.fieldname] = []; + } + filesGrouped[file.fieldname].push(file); + }); + } + + console.log('๐Ÿ“ธ Files grouped:', Object.keys(filesGrouped)); + + // Process each variant + productData.variants = await Promise.all( + parsedVariants.map(async (variant) => { + const color = variant.color; + const fieldName = `variantImages_${color}`; + + console.log(`๐Ÿ” Looking for images with fieldname: ${fieldName}`); + + // Get images for this variant + const variantFiles = filesGrouped[fieldName] || []; + console.log(`๐Ÿ“ธ Found ${variantFiles.length} images for ${color}`); + + // Upload images to S3 + const variantImages = variantFiles.length > 0 + ? await Promise.all(variantFiles.map(uploadToS3)) + : []; + + console.log(`โœ… Uploaded ${variantImages.length} images for ${color}`); + + return { + size: variant.size || 'default', + color: variant.color, + sku: variant.sku, + price: Number(variant.price), + compareAtPrice: variant.compareAtPrice ? Number(variant.compareAtPrice) : null, + inventory: { + quantity: Number(variant.quantity || variant.stock || 0), + trackInventory: true, + }, + images: variantImages, + isActive: true, + }; + }) + ); + + console.log('โœ… All variants processed:', productData.variants.length); + } + // ====================== + // SIMPLE PRODUCT MODE + // ====================== + else { + console.log('๐Ÿ“ฆ Processing simple product...'); + + // โœ… Handle files from req.files array + let primaryImage = null; + let galleryImages = []; + + if (req.files && req.files.length > 0) { + // Group files by fieldname + const filesGrouped = {}; + req.files.forEach(file => { + if (!filesGrouped[file.fieldname]) { + filesGrouped[file.fieldname] = []; + } + filesGrouped[file.fieldname].push(file); + }); + + // Upload primary image + if (filesGrouped['primaryImage'] && filesGrouped['primaryImage'][0]) { + primaryImage = await uploadToS3(filesGrouped['primaryImage'][0]); + } + + // Upload gallery images + if (filesGrouped['galleryImages']) { + galleryImages = await Promise.all( + filesGrouped['galleryImages'].map(uploadToS3) + ); + } + } + + productData.images = { + primary: primaryImage, + gallery: galleryImages, + videos: [], + }; + + console.log('โœ… Images uploaded:', { + primary: !!primaryImage, + gallery: galleryImages.length, + }); + } + + // Create product in MongoDB + const product = await Product.create(productData); + + console.log('โœ… Product created:', product._id); + + res.status(201).json({ + status: true, + message: "Product created successfully", + data: product, + }); + } catch (err) { + console.error('โŒ Error creating product:', err); + + // Send detailed error for debugging + if (process.env.NODE_ENV === 'development') { + return res.status(400).json({ + status: false, + message: "Failed to create product", + error: err.message, + stack: err.stack, + }); + } + + next(err); + } +}; + + +exports.updateProduct = async (req, res, next) => { + try { + const product = await Product.findByIdAndUpdate(req.params.id, req.body, { + new: true, + }); + // if (!product) return res.status(404).json({ success: false, message: 'Product not found' }); + if (!product) { + return res.status(404).json({ + statusCode: 404, + status: false, + message: 'Product not found', + }); + } + // res.json({ success: true, message: 'Product updated', data: product }); + return res.status(200).json({ + statusCode: 200, + status: true, + message: 'Product updated successfully', + data: product, + }); + } catch (error) { + next(error); + } +}; + +exports.deleteProduct = async (req, res, next) => { + try { + const product = await Product.findByIdAndDelete(req.params.id); + // if (!product) return res.status(404).json({ success: false, message: 'Product not found' }); + if (!product) { + return res.status(404).json({ + statusCode: 404, + status: false, + message: 'Product not found', + }); + } + // res.json({ success: true, message: 'Product deleted' }); + return res.status(200).json({ + statusCode: 200, + status: true, + message: 'Product deleted successfully', + }); + } catch (error) { + next(error); + } +}; diff --git a/src/controllers/admin/reportController.js b/src/controllers/admin/reportController.js new file mode 100644 index 0000000..b390160 --- /dev/null +++ b/src/controllers/admin/reportController.js @@ -0,0 +1,1170 @@ +// const { prisma } = require('../../config/database'); +// const Product = require('../../models/mongodb/Product'); +// // const ReturnModel = require("../../models/mongodb/Return"); +// const Order = prisma.order; + +// module.exports = { +// // =============================== +// // ๐Ÿ“Œ 1. OVERVIEW KPI +// // =============================== +// getOverviewReport: async (req, res, next) => { +// try { +// const [ +// totalUsers, +// totalOrders, +// totalProducts, +// totalRevenue, +// totalCustomers, +// totalSellers, +// ] = await Promise.all([ +// prisma.user.count(), +// prisma.order.count(), +// Product.countDocuments(), +// prisma.order.aggregate({ +// _sum: { totalAmount: true }, +// where: { paymentStatus: 'PAID' }, +// }), +// prisma.user.count({ where: { role: 'CUSTOMER' } }), +// prisma.user.count({ where: { role: 'SELLER' } }), +// ]); + +// res.json({ +// success: true, +// data: { +// totalUsers, +// totalCustomers, +// totalSellers, +// totalOrders, +// totalProducts, +// totalRevenue: totalRevenue._sum.totalAmount || 0, +// }, +// }); +// } catch (error) { +// next(error); +// } +// }, + +// // =============================== +// // ๐Ÿ“Œ 2. SALES ANALYTICS (Daily + Monthly) +// // =============================== +// getSalesAnalytics: async (req, res, next) => { +// try { +// const today = new Date(); +// const last30Days = new Date(); +// last30Days.setDate(today.getDate() - 30); + +// // 1๏ธโƒฃ Daily sales (last 30 days) +// const dailyOrders = await prisma.order.findMany({ +// where: { +// paymentStatus: 'PAID', +// createdAt: { gte: last30Days }, +// }, +// select: { +// totalAmount: true, +// createdAt: true, +// }, +// }); + +// const dailySales = dailyOrders.reduce((acc, order) => { +// const date = order.createdAt.toISOString().split('T')[0]; // YYYY-MM-DD +// acc[date] = (acc[date] || 0) + Number(order.totalAmount); +// return acc; +// }, {}); + +// // 2๏ธโƒฃ Monthly sales (all time) +// const allPaidOrders = await prisma.order.findMany({ +// where: { paymentStatus: 'PAID' }, +// select: { totalAmount: true, createdAt: true }, +// }); + +// const monthlySales = allPaidOrders.reduce((acc, order) => { +// const month = order.createdAt.getMonth() + 1; // 1-12 +// const year = order.createdAt.getFullYear(); +// const key = `${year}-${month.toString().padStart(2, '0')}`; // YYYY-MM +// acc[key] = (acc[key] || 0) + Number(order.totalAmount); +// return acc; +// }, {}); + +// res.json({ +// success: true, +// data: { +// dailySales, +// monthlySales, +// }, +// }); +// } catch (error) { +// next(error); +// } +// }, + +// // =============================== +// // ๐Ÿ“Œ 3. CUSTOMER STATISTICS +// // =============================== +// // =============================== +// // ๐Ÿ“Œ 3. CUSTOMER STATISTICS (UPDATED) +// // =============================== +// getCustomerStats: async (req, res, next) => { +// try { +// const now = new Date(); + +// // ---------- WEEKLY DATA (Monโ€“Sun) ---------- +// const startOfWeek = new Date(now); +// startOfWeek.setDate(now.getDate() - now.getDay() + 1); // Monday + +// const weeklyData = await Promise.all( +// Array.from({ length: 7 }).map(async (_, i) => { +// const dayStart = new Date(startOfWeek); +// dayStart.setDate(startOfWeek.getDate() + i); + +// const dayEnd = new Date(dayStart); +// dayEnd.setDate(dayStart.getDate() + 1); + +// const count = await prisma.user.count({ +// where: { +// role: 'CUSTOMER', +// createdAt: { +// gte: dayStart, +// lt: dayEnd, +// }, +// }, +// }); + +// const dayLabels = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']; +// return { label: dayLabels[i], value: count }; +// }) +// ); + +// // ---------- MONTHLY DATA (Janโ€“Dec) ---------- +// const monthlyData = await Promise.all( +// Array.from({ length: 12 }).map(async (_, i) => { +// const monthStart = new Date(now.getFullYear(), i, 1); +// const monthEnd = new Date(now.getFullYear(), i + 1, 1); + +// const count = await prisma.user.count({ +// where: { +// role: 'CUSTOMER', +// createdAt: { +// gte: monthStart, +// lt: monthEnd, +// }, +// }, +// }); + +// const monthLabels = [ +// 'Jan', +// 'Feb', +// 'Mar', +// 'Apr', +// 'May', +// 'Jun', +// 'Jul', +// 'Aug', +// 'Sep', +// 'Oct', +// 'Nov', +// 'Dec', +// ]; +// return { label: monthLabels[i], value: count }; +// }) +// ); + +// // ---------- YEARLY DATA (Last 5 Years) ---------- +// const currentYear = now.getFullYear(); +// const yearlyData = await Promise.all( +// Array.from({ length: 5 }).map(async (_, i) => { +// const year = currentYear - (4 - i); + +// const yearStart = new Date(year, 0, 1); +// const yearEnd = new Date(year + 1, 0, 1); + +// const count = await prisma.user.count({ +// where: { +// role: 'CUSTOMER', +// createdAt: { +// gte: yearStart, +// lt: yearEnd, +// }, +// }, +// }); + +// return { label: year.toString(), value: count }; +// }) +// ); + +// // ---------- CURRENT STATS ---------- +// const [newCustomers, repeatCustomers] = await Promise.all([ +// prisma.user.count({ +// where: { +// role: 'CUSTOMER', +// createdAt: { gte: new Date(Date.now() - 7 * 24 * 60 * 60 * 1000) }, +// }, +// }), + +// prisma.order.groupBy({ +// by: ['userId'], +// _count: { id: true }, +// having: { id: { _count: { gt: 1 } } }, +// }), +// ]); + +// // ---------- RESPONSE ---------- +// res.json({ +// success: true, +// data: { +// newCustomers, +// repeatCustomers: repeatCustomers.length, + +// graph: { +// weekly: weeklyData, +// monthly: monthlyData, +// yearly: yearlyData, +// }, +// }, +// }); +// } catch (error) { +// console.error(error); +// next(error); +// } +// }, + +// // =============================== +// // ๐Ÿ“Œ 4. SELLER STATISTICS +// // =============================== +// getSellerStats: async (req, res, next) => { +// try { +// // Fetch all sellers from PostgreSQL +// const sellers = await prisma.user.findMany({ +// where: { role: 'SELLER' }, +// }); + +// // For each seller, count products from MongoDB +// const formatted = await Promise.all( +// sellers.map(async s => { +// const totalProducts = await Product.countDocuments({ +// sellerId: s.id, +// }); +// return { +// sellerId: s.id, +// name: `${s.firstName} ${s.lastName}`, +// totalProducts, +// }; +// }) +// ); + +// res.json({ success: true, data: formatted }); +// } catch (error) { +// next(error); +// } +// }, + +// // =============================== +// // ๐Ÿ“Œ 5. ORDER ANALYTICS +// // =============================== +// getOrderAnalytics: async (req, res, next) => { +// try { +// const orders = await prisma.order.groupBy({ +// by: ['status'], +// _count: { id: true }, +// }); + +// res.json({ success: true, data: orders }); +// } catch (error) { +// next(error); +// } +// }, + +// // =============================== +// // ๐Ÿ“Œ 6. RETURN / REFUND REPORT +// // =============================== +// // getReturnAnalytics: async (req, res, next) => { +// // try { +// // const totalReturns = await ReturnModel.countDocuments(); + +// // const returnReasons = await ReturnModel.aggregate([ +// // { +// // $group: { +// // _id: "$reason", +// // count: { $sum: 1 }, +// // }, +// // }, +// // ]); + +// // res.json({ +// // success: true, +// // data: { +// // totalReturns, +// // returnReasons, +// // }, +// // }); +// // } catch (error) { +// // next(error); +// // } +// // }, + +// // =============================== +// // ๐Ÿ“Œ 7. INVENTORY & STOCK REPORT +// // =============================== +// // Controller: getInventoryStats +// getInventoryStats: async (req, res, next) => { +// try { +// const [lowStock, outOfStock, fastMoving] = await Promise.all([ +// Product.find({ stock: { $lte: 5, $gt: 0 } }) +// .select('_id name stock category') +// .populate('category', 'name'), // get only category name +// Product.find({ stock: 0 }) +// .select('_id name stock category') +// .populate('category', 'name'), +// Product.find() +// .sort({ purchaseCount: -1 }) +// .limit(10) +// .select('_id name stock category purchaseCount') +// .populate('category', 'name'), +// ]); + +// res.json({ +// success: true, +// data: { +// lowStock, +// outOfStock, +// fastMoving, +// }, +// }); +// } catch (error) { +// next(error); +// } +// }, + +// // =============================== +// // ๐Ÿ“Œ Financial Stats (Safe Version) +// // =============================== +// getFinancialStats: async (req, res, next) => { +// try { +// // Total revenue from PAID orders +// const totalRevenue = await prisma.order.aggregate({ +// _sum: { totalAmount: true }, +// where: { paymentStatus: 'PAID' }, +// }); + +// // Total number of orders +// const totalOrders = await prisma.order.count(); + +// // Total number of paid orders +// const paidOrders = await prisma.order.count({ +// where: { paymentStatus: 'PAID' }, +// }); + +// res.json({ +// success: true, +// data: { +// totalRevenue: totalRevenue._sum.totalAmount || 0, +// totalOrders, +// paidOrders, +// }, +// }); +// } catch (error) { +// next(error); +// } +// }, + +// // =============================== +// // ๐Ÿ“Œ 9. PAYOUT / TRANSFER HISTORY +// // =============================== +// getPayoutHistory: async (req, res, next) => { +// try { +// // Fetch all delivered orders with user info +// const orders = await prisma.order.findMany({ +// where: { status: 'DELIVERED' }, +// include: { user: true }, +// orderBy: { deliveredAt: 'desc' }, +// }); + +// const formatted = orders.map(order => ({ +// orderId: order.id, +// orderNumber: order.orderNumber, +// sellerId: order.userId, +// sellerName: `${order.user.firstName} ${order.user.lastName}`, +// totalAmount: Number(order.totalAmount), +// deliveredAt: order.deliveredAt, +// })); + +// res.json({ success: true, data: formatted }); +// } catch (error) { +// next(error); +// } +// }, + +// // =============================== +// // ๐Ÿ“Œ 10. REAL-TIME ACTIVITY FEED +// // =============================== +// getActivityFeed: async (req, res, next) => { +// try { +// const logs = await prisma.activityLog.findMany({ +// orderBy: { createdAt: 'desc' }, +// take: 20, +// }); + +// res.json({ success: true, data: logs }); +// } catch (error) { +// next(error); +// } +// }, +// }; + + + + +const { prisma } = require('../../config/database'); +const Product = require('../../models/mongodb/Product'); + +// โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +// ๐Ÿ›  HELPERS +// โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + +/** + * Parse optional ?from= and ?to= query params into Date objects. + * Defaults: from = 30 days ago, to = now + */ +function parseDateRange(query) { + const to = query.to ? new Date(query.to) : new Date(); + const from = query.from + ? new Date(query.from) + : new Date(Date.now() - 30 * 24 * 60 * 60 * 1000); + + // Set 'to' to end of that day + to.setHours(23, 59, 59, 999); + from.setHours(0, 0, 0, 0); + + return { from, to }; +} + +/** + * Parse pagination params from query string. + * Defaults: page=1, limit=20 + */ +function parsePagination(query) { + const page = Math.max(1, parseInt(query.page) || 1); + const limit = Math.min(100, Math.max(1, parseInt(query.limit) || 20)); + const skip = (page - 1) * limit; + return { page, limit, skip }; +} + +// โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +// ๐Ÿ“Œ 1. OVERVIEW KPI +// GET /api/admin/reports/overview +// โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +const getOverviewReport = async (req, res, next) => { + try { + const { from, to } = parseDateRange(req.query); + + const [ + totalUsers, + totalOrders, + totalProducts, + totalRevenue, + totalCustomers, + totalSellers, + newUsersInRange, + ordersInRange, + revenueInRange, + ] = await Promise.all([ + prisma.user.count(), + prisma.order.count(), + Product.countDocuments(), + + // All-time revenue + prisma.order.aggregate({ + _sum: { totalAmount: true }, + where: { paymentStatus: 'PAID' }, + }), + + prisma.user.count({ where: { role: 'CUSTOMER' } }), + prisma.user.count({ where: { role: 'SELLER' } }), + + // New users in date range + prisma.user.count({ + where: { createdAt: { gte: from, lte: to } }, + }), + + // Orders in date range + prisma.order.count({ + where: { createdAt: { gte: from, lte: to } }, + }), + + // Revenue in date range + prisma.order.aggregate({ + _sum: { totalAmount: true }, + where: { + paymentStatus: 'PAID', + createdAt: { gte: from, lte: to }, + }, + }), + ]); + + res.json({ + success: true, + data: { + allTime: { + totalUsers, + totalCustomers, + totalSellers, + totalOrders, + totalProducts, + totalRevenue: Number(totalRevenue._sum.totalAmount) || 0, + }, + inRange: { + from, + to, + newUsers: newUsersInRange, + orders: ordersInRange, + revenue: Number(revenueInRange._sum.totalAmount) || 0, + }, + }, + }); + } catch (error) { + next(error); + } +}; + +// โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +// ๐Ÿ“Œ 2. SALES ANALYTICS +// GET /api/admin/reports/sales?from=&to=&groupBy=daily|monthly +// โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +const getSalesAnalytics = async (req, res, next) => { + try { + const { from, to } = parseDateRange(req.query); + const groupBy = req.query.groupBy === 'monthly' ? 'monthly' : 'daily'; + + const paidOrders = await prisma.order.findMany({ + where: { + paymentStatus: 'PAID', + createdAt: { gte: from, lte: to }, + }, + select: { + totalAmount: true, + createdAt: true, + status: true, + }, + orderBy: { createdAt: 'asc' }, + }); + + // Group by day or month + const grouped = paidOrders.reduce((acc, order) => { + const key = + groupBy === 'monthly' + ? order.createdAt.toISOString().slice(0, 7) // YYYY-MM + : order.createdAt.toISOString().slice(0, 10); // YYYY-MM-DD + if (!acc[key]) acc[key] = { revenue: 0, orders: 0 }; + acc[key].revenue += Number(order.totalAmount); + acc[key].orders += 1; + return acc; + }, {}); + + // Order status breakdown in range + const statusBreakdown = await prisma.order.groupBy({ + by: ['status'], + _count: { id: true }, + where: { createdAt: { gte: from, lte: to } }, + }); + + // Average order value + const avgOrderValue = + paidOrders.length > 0 + ? paidOrders.reduce((sum, o) => sum + Number(o.totalAmount), 0) / + paidOrders.length + : 0; + + res.json({ + success: true, + data: { + groupBy, + from, + to, + totalOrders: paidOrders.length, + avgOrderValue: parseFloat(avgOrderValue.toFixed(2)), + sales: grouped, + statusBreakdown: statusBreakdown.map(s => ({ + status: s.status, + count: s._count.id, + })), + }, + }); + } catch (error) { + next(error); + } +}; + +// โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +// ๐Ÿ“Œ 3. CUSTOMER STATISTICS +// GET /api/admin/reports/customers?view=weekly|monthly|yearly +// โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +const getCustomerStats = async (req, res, next) => { + try { + const now = new Date(); + const view = ['weekly', 'monthly', 'yearly'].includes(req.query.view) + ? req.query.view + : 'monthly'; + + let graphData = []; + + if (view === 'weekly') { + // Monโ€“Sun of current week + const startOfWeek = new Date(now); + startOfWeek.setDate(now.getDate() - now.getDay() + 1); + startOfWeek.setHours(0, 0, 0, 0); + + const dayLabels = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']; + graphData = await Promise.all( + dayLabels.map(async (label, i) => { + const dayStart = new Date(startOfWeek); + dayStart.setDate(startOfWeek.getDate() + i); + const dayEnd = new Date(dayStart); + dayEnd.setDate(dayStart.getDate() + 1); + + const count = await prisma.user.count({ + where: { + role: 'CUSTOMER', + createdAt: { gte: dayStart, lt: dayEnd }, + }, + }); + return { label, value: count }; + }) + ); + } else if (view === 'monthly') { + // Janโ€“Dec of current year + const monthLabels = ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec']; + graphData = await Promise.all( + monthLabels.map(async (label, i) => { + const monthStart = new Date(now.getFullYear(), i, 1); + const monthEnd = new Date(now.getFullYear(), i + 1, 1); + const count = await prisma.user.count({ + where: { + role: 'CUSTOMER', + createdAt: { gte: monthStart, lt: monthEnd }, + }, + }); + return { label, value: count }; + }) + ); + } else { + // Last 5 years + graphData = await Promise.all( + Array.from({ length: 5 }, (_, i) => now.getFullYear() - (4 - i)).map( + async year => { + const count = await prisma.user.count({ + where: { + role: 'CUSTOMER', + createdAt: { + gte: new Date(year, 0, 1), + lt: new Date(year + 1, 0, 1), + }, + }, + }); + return { label: String(year), value: count }; + } + ) + ); + } + + const [newCustomers, repeatCustomers, totalCustomers] = await Promise.all([ + // New in last 7 days + prisma.user.count({ + where: { + role: 'CUSTOMER', + createdAt: { gte: new Date(Date.now() - 7 * 24 * 60 * 60 * 1000) }, + }, + }), + + // Users with more than 1 order + prisma.order.groupBy({ + by: ['userId'], + _count: { id: true }, + having: { id: { _count: { gt: 1 } } }, + }), + + prisma.user.count({ where: { role: 'CUSTOMER' } }), + ]); + + res.json({ + success: true, + data: { + total: totalCustomers, + newThisWeek: newCustomers, + repeatCustomers: repeatCustomers.length, + graph: { view, data: graphData }, + }, + }); + } catch (error) { + next(error); + } +}; + +// โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +// ๐Ÿ“Œ 4. SELLER STATISTICS +// GET /api/admin/reports/sellers?page=1&limit=20 +// โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +const getSellerStats = async (req, res, next) => { + try { + const { page, limit, skip } = parsePagination(req.query); + + const [sellers, totalSellers] = await Promise.all([ + prisma.user.findMany({ + where: { role: 'SELLER' }, + select: { + id: true, + firstName: true, + lastName: true, + email: true, + createdAt: true, + }, + skip, + take: limit, + orderBy: { createdAt: 'desc' }, + }), + prisma.user.count({ where: { role: 'SELLER' } }), + ]); + + // Batch fetch product counts from MongoDB (avoids N+1) + const sellerIds = sellers.map(s => s.id); + const productCounts = await Product.aggregate([ + { $match: { sellerId: { $in: sellerIds } } }, + { $group: { _id: '$sellerId', count: { $sum: 1 } } }, + ]); + + const countMap = productCounts.reduce((acc, item) => { + acc[item._id] = item.count; + return acc; + }, {}); + + const formatted = sellers.map(s => ({ + sellerId: s.id, + name: `${s.firstName} ${s.lastName}`, + email: s.email, + joinedAt: s.createdAt, + totalProducts: countMap[s.id] || 0, + })); + + res.json({ + success: true, + data: formatted, + pagination: { + page, + limit, + total: totalSellers, + totalPages: Math.ceil(totalSellers / limit), + }, + }); + } catch (error) { + next(error); + } +}; + +// โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +// ๐Ÿ“Œ 5. ORDER ANALYTICS +// GET /api/admin/reports/orders?from=&to= +// โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +const getOrderAnalytics = async (req, res, next) => { + try { + const { from, to } = parseDateRange(req.query); + + const [statusBreakdown, paymentBreakdown, totalInRange, totalAllTime] = + await Promise.all([ + prisma.order.groupBy({ + by: ['status'], + _count: { id: true }, + where: { createdAt: { gte: from, lte: to } }, + }), + prisma.order.groupBy({ + by: ['paymentStatus'], + _count: { id: true }, + _sum: { totalAmount: true }, + where: { createdAt: { gte: from, lte: to } }, + }), + prisma.order.count({ + where: { createdAt: { gte: from, lte: to } }, + }), + prisma.order.count(), + ]); + + res.json({ + success: true, + data: { + from, + to, + totalOrders: totalInRange, + totalOrdersAllTime: totalAllTime, + byStatus: statusBreakdown.map(s => ({ + status: s.status, + count: s._count.id, + })), + byPaymentStatus: paymentBreakdown.map(p => ({ + paymentStatus: p.paymentStatus, + count: p._count.id, + totalAmount: Number(p._sum.totalAmount) || 0, + })), + }, + }); + } catch (error) { + next(error); + } +}; + +// โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +// ๐Ÿ“Œ 6. RETURN / REFUND ANALYTICS +// GET /api/admin/reports/returns +// Uncomment and wire up once ReturnModel is ready +// โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +// const getReturnAnalytics = async (req, res, next) => { +// try { +// const ReturnModel = require('../../models/mongodb/Return'); +// const { from, to } = parseDateRange(req.query); +// +// const [totalReturns, returnReasons, returnsByStatus] = await Promise.all([ +// ReturnModel.countDocuments({ createdAt: { $gte: from, $lte: to } }), +// ReturnModel.aggregate([ +// { $match: { createdAt: { $gte: from, $lte: to } } }, +// { $group: { _id: '$reason', count: { $sum: 1 } } }, +// { $sort: { count: -1 } }, +// ]), +// ReturnModel.aggregate([ +// { $match: { createdAt: { $gte: from, $lte: to } } }, +// { $group: { _id: '$status', count: { $sum: 1 } } }, +// ]), +// ]); +// +// res.json({ +// success: true, +// data: { totalReturns, returnReasons, returnsByStatus }, +// }); +// } catch (error) { +// next(error); +// } +// }; + +// โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +// ๐Ÿ“Œ 7. INVENTORY & STOCK REPORT +// GET /api/admin/reports/inventory?page=1&limit=20 +// โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +const getInventoryStats = async (req, res, next) => { + try { + const { page, limit, skip } = parsePagination(req.query); + + const [lowStock, outOfStock, fastMoving, totalProducts] = await Promise.all([ + Product.find({ stock: { $lte: 5, $gt: 0 } }) + .select('_id name stock category') + .populate('category', 'name') + .skip(skip) + .limit(limit) + .lean(), + + Product.find({ stock: 0 }) + .select('_id name stock category') + .populate('category', 'name') + .skip(skip) + .limit(limit) + .lean(), + + Product.find() + .sort({ purchaseCount: -1 }) + .limit(10) + .select('_id name stock category purchaseCount') + .populate('category', 'name') + .lean(), + + Product.countDocuments(), + ]); + + const lowStockCount = await Product.countDocuments({ stock: { $lte: 5, $gt: 0 } }); + const outOfStockCount = await Product.countDocuments({ stock: 0 }); + + res.json({ + success: true, + data: { + summary: { + totalProducts, + lowStockCount, + outOfStockCount, + healthyStockCount: totalProducts - lowStockCount - outOfStockCount, + }, + lowStock, + outOfStock, + fastMoving, + }, + pagination: { page, limit }, + }); + } catch (error) { + next(error); + } +}; + +// โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +// ๐Ÿ“Œ 8. FINANCIAL STATS +// GET /api/admin/reports/financial?from=&to= +// โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +const getFinancialStats = async (req, res, next) => { + try { + const { from, to } = parseDateRange(req.query); + + const [ + totalRevenueAll, + revenueInRange, + pendingPayments, + totalOrders, + paidOrders, + failedOrders, + ] = await Promise.all([ + // All-time total revenue + prisma.order.aggregate({ + _sum: { totalAmount: true }, + where: { paymentStatus: 'PAID' }, + }), + + // Revenue in date range + prisma.order.aggregate({ + _sum: { totalAmount: true }, + where: { + paymentStatus: 'PAID', + createdAt: { gte: from, lte: to }, + }, + }), + + // Pending payments (money not yet collected) + prisma.order.aggregate({ + _sum: { totalAmount: true }, + where: { paymentStatus: 'PENDING' }, + }), + + prisma.order.count({ where: { createdAt: { gte: from, lte: to } } }), + + prisma.order.count({ + where: { paymentStatus: 'PAID', createdAt: { gte: from, lte: to } }, + }), + + prisma.order.count({ + where: { paymentStatus: 'FAILED', createdAt: { gte: from, lte: to } }, + }), + ]); + + const revenueVal = Number(revenueInRange._sum.totalAmount) || 0; + const avgOrderValue = paidOrders > 0 ? revenueVal / paidOrders : 0; + + res.json({ + success: true, + data: { + allTime: { + totalRevenue: Number(totalRevenueAll._sum.totalAmount) || 0, + }, + inRange: { + from, + to, + revenue: revenueVal, + pendingRevenue: Number(pendingPayments._sum.totalAmount) || 0, + totalOrders, + paidOrders, + failedOrders, + avgOrderValue: parseFloat(avgOrderValue.toFixed(2)), + conversionRate: + totalOrders > 0 + ? parseFloat(((paidOrders / totalOrders) * 100).toFixed(2)) + : 0, + }, + }, + }); + } catch (error) { + next(error); + } +}; + +// โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +// ๐Ÿ“Œ 9. PAYOUT HISTORY +// GET /api/admin/reports/payouts?page=1&limit=20&from=&to= +// โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +const getPayoutHistory = async (req, res, next) => { + try { + const { page, limit, skip } = parsePagination(req.query); + const { from, to } = parseDateRange(req.query); + + const [orders, total] = await Promise.all([ + prisma.order.findMany({ + where: { + status: 'DELIVERED', + deliveredAt: { gte: from, lte: to }, + }, + include: { + user: { + select: { id: true, firstName: true, lastName: true, email: true }, + }, + }, + orderBy: { deliveredAt: 'desc' }, + skip, + take: limit, + }), + prisma.order.count({ + where: { + status: 'DELIVERED', + deliveredAt: { gte: from, lte: to }, + }, + }), + ]); + + const formatted = orders.map(order => ({ + orderId: order.id, + orderNumber: order.orderNumber, + customer: { + id: order.user.id, + name: `${order.user.firstName} ${order.user.lastName}`, + email: order.user.email, + }, + totalAmount: Number(order.totalAmount), + deliveredAt: order.deliveredAt, + })); + + res.json({ + success: true, + data: formatted, + pagination: { + page, + limit, + total, + totalPages: Math.ceil(total / limit), + }, + }); + } catch (error) { + next(error); + } +}; + +// โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +// ๐Ÿ“Œ 10. ACTIVITY FEED +// GET /api/admin/reports/activity?limit=20 +// NOTE: Requires 'activityLog' model in your Prisma schema. +// Add this to schema.prisma if missing: +// +// model ActivityLog { +// id String @id @default(cuid()) +// action String +// userId String? +// meta Json? +// createdAt DateTime @default(now()) +// } +// โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +const getActivityFeed = async (req, res, next) => { + try { + const limit = Math.min(100, parseInt(req.query.limit) || 20); + + // Guard: check if activityLog exists in prisma client + if (!prisma.activityLog) { + return res.status(501).json({ + success: false, + message: + 'ActivityLog model not found. Please add it to your Prisma schema and run `npx prisma migrate dev`.', + }); + } + + const logs = await prisma.activityLog.findMany({ + orderBy: { createdAt: 'desc' }, + take: limit, + }); + + res.json({ success: true, data: logs }); + } catch (error) { + next(error); + } +}; + +// โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +// ๐Ÿ“Œ 11. TOP PRODUCTS REPORT โ† NEW +// GET /api/admin/reports/top-products?limit=10&from=&to= +// โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +const getTopProducts = async (req, res, next) => { + try { + const limit = Math.min(50, parseInt(req.query.limit) || 10); + + const topProducts = await Product.find() + .sort({ purchaseCount: -1 }) + .limit(limit) + .select('_id name stock purchaseCount price category images') + .populate('category', 'name') + .lean(); + + res.json({ + success: true, + data: topProducts.map(p => ({ + productId: p._id, + name: p.name, + category: p.category?.name || 'Uncategorized', + stock: p.stock, + purchaseCount: p.purchaseCount || 0, + price: p.price, + thumbnail: p.images?.[0] || null, + })), + }); + } catch (error) { + next(error); + } +}; + +// โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +// ๐Ÿ“Œ 12. COUPON USAGE REPORT โ† NEW +// GET /api/admin/reports/coupons?from=&to= +// โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +const getCouponStats = async (req, res, next) => { + try { + const { from, to } = parseDateRange(req.query); + + // Assumes Order has a relation to Coupon or stores couponCode + const ordersWithCoupon = await prisma.order.findMany({ + where: { + couponCode: { not: null }, + createdAt: { gte: from, lte: to }, + }, + select: { + couponCode: true, + totalAmount: true, + discountAmount: true, + }, + }); + + const couponMap = ordersWithCoupon.reduce((acc, order) => { + const code = order.couponCode; + if (!acc[code]) acc[code] = { usageCount: 0, totalDiscount: 0, totalRevenue: 0 }; + acc[code].usageCount += 1; + acc[code].totalDiscount += Number(order.discountAmount) || 0; + acc[code].totalRevenue += Number(order.totalAmount) || 0; + return acc; + }, {}); + + const ranked = Object.entries(couponMap) + .map(([code, stats]) => ({ code, ...stats })) + .sort((a, b) => b.usageCount - a.usageCount); + + res.json({ + success: true, + data: { + from, + to, + totalCouponOrders: ordersWithCoupon.length, + coupons: ranked, + }, + }); + } catch (error) { + next(error); + } +}; + +// โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +// EXPORTS +// โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +module.exports = { + getOverviewReport, + getSalesAnalytics, + getCustomerStats, + getSellerStats, + getOrderAnalytics, + getInventoryStats, + getFinancialStats, + getPayoutHistory, + getActivityFeed, + getTopProducts, + getCouponStats, + // getReturnAnalytics, // uncomment when ReturnModel is ready +}; \ No newline at end of file diff --git a/src/controllers/admin/userController.js b/src/controllers/admin/userController.js new file mode 100644 index 0000000..5f2962b --- /dev/null +++ b/src/controllers/admin/userController.js @@ -0,0 +1,160 @@ +const { prisma } = require('../../config/database'); + +exports.getAllUsers = async (req, res, next) => { + try { + const { page = 1, limit = 20, role, search, isActive } = req.query; + const skip = (page - 1) * limit; + + const where = {}; + if (role) where.role = role; + if (isActive !== undefined) where.isActive = isActive === 'true'; + if (search) { + where.OR = [ + { email: { contains: search, mode: 'insensitive' } }, + { firstName: { contains: search, mode: 'insensitive' } }, + { lastName: { contains: search, mode: 'insensitive' } }, + { username: { contains: search, mode: 'insensitive' } }, + ]; + } + + const [users, total] = await Promise.all([ + prisma.user.findMany({ + where, + select: { + id: true, + email: true, + username: true, + firstName: true, + lastName: true, + role: true, + isVerified: true, + isActive: true, + createdAt: true, + lastLoginAt: true, + }, + orderBy: { createdAt: 'desc' }, + skip: parseInt(skip), + take: parseInt(limit), + }), + prisma.user.count({ where }), + ]); + + // res.json({ + // success: true, + // data: { users, pagination: { page: +page, limit: +limit, total, pages: Math.ceil(total / limit) } }, + // }); + return res.status(200).json({ + statusCode: 200, + status: true, + message: 'Users fetched successfully', + data: { + users, + pagination: { + page: +page, + limit: +limit, + total, + pages: Math.ceil(total / limit), + }, + }, + }); + } catch (error) { + next(error); + } +}; + +exports.updateUserStatus = async (req, res, next) => { + try { + const { isActive, role } = req.body; + const { id } = req.params; + + const user = await prisma.user.findUnique({ where: { id } }); + // if (!user) return res.status(404).json({ success: false, message: 'User not found' }); + + if (!user) { + return res.status(404).json({ + statusCode: 404, + status: false, + message: 'User not found', + }); + } + + const updatedUser = await prisma.user.update({ + where: { id }, + data: { + ...(isActive !== undefined && { isActive }), + ...(role && { role }), + }, + }); + + // res.json({ success: true, message: 'User updated successfully', data: updatedUser }); + return res.status(200).json({ + statusCode: 200, + status: true, + message: 'User updated successfully', + data: updatedUser, + }); + } catch (error) { + next(error); + } +}; + +exports.getUserById = async (req, res, next) => { + try { + const { id } = req.params; + + // if (!id) { + // return res.status(400).json({ + // success: false, + // message: "User ID is required", + // }); + // } + + if (!id) { + return res.status(400).json({ + statusCode: 400, + status: false, + message: 'User ID is required', + }); + } + + const user = await prisma.user.findUnique({ + where: { id }, + select: { + id: true, + email: true, + username: true, + firstName: true, + lastName: true, + role: true, + isVerified: true, + isActive: true, + createdAt: true, + lastLoginAt: true, + }, + }); + + // if (!user) { + // return res.status(404).json({ + // success: false, + // message: "User not found", + // }); + // } + if (!user) { + return res.status(404).json({ + statusCode: 404, + status: false, + message: 'User not found', + }); + } + + // res.json({ success: true, data: { user } }); + return res.status(200).json({ + statusCode: 200, + status: true, + message: 'User details fetched successfully', + data: { user }, + }); + } catch (error) { + next(error); + } +}; diff --git a/src/controllers/authController.js b/src/controllers/authController.js new file mode 100644 index 0000000..0f266e7 --- /dev/null +++ b/src/controllers/authController.js @@ -0,0 +1,421 @@ +const authService = require('../services/authService'); +const { generateOTP, sendOTP } = require('../services/wappconnectService'); +const { saveOTP, verifyOTP } = require('../services/otpStore'); +const { prisma } = require('../config/database'); + +// @desc Register user +exports.register = async (req, res, next) => { + try { + const { email, password, firstName, lastName, username, phone, role } = + req.body; + + if (!email || !password) { + return res.status(400).json({ + statusCode: 400, + status: false, + message: 'Email and password are required', + }); + } + + if (password.length < 6) { + return res.status(400).json({ + statusCode: 400, + status: false, + message: 'Password must be at least 6 characters', + }); + } + + const result = await authService.register({ + email, + password, + firstName, + lastName, + username, + phone, + role, + }); + + return res.status(201).json({ + statusCode: 201, + status: true, + message: 'User registered successfully', + data: result, + }); + } catch (error) { + return res.status(500).json({ + statusCode: 500, + status: false, + message: error.message || 'Internal server error', + }); + } +}; + +// @desc Login user +exports.login = async (req, res, next) => { + try { + const { email, password } = req.body; + + if (!email || !password) { + return res.status(400).json({ + statusCode: 400, + status: false, + message: 'Email and password are required', + }); + } + + const result = await authService.login(email, password); + + return res.status(200).json({ + statusCode: 200, + status: true, + message: 'Login successful', + data: result, + }); + } catch (error) { + return res.status(500).json({ + statusCode: 500, + status: false, + message: error.message || 'Internal server error', + }); + } +}; + +// โ”€โ”€โ”€ STEP 1: Request OTP โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +exports.sendLoginOTP = async (req, res, next) => { + try { + const { phone } = req.body; + + if (!phone) { + return res.status(400).json({ + status: false, + message: 'Phone number is required', + }); + } + + const user = await prisma.user.findFirst({ where: { phone } }); + if (!user) { + return res.status(404).json({ + status: false, + message: 'No account found with this phone number', + }); + } + + if (!user.isActive) { + return res.status(403).json({ + status: false, + message: 'Account is deactivated', + }); + } + + const otp = generateOTP(); + saveOTP(phone, otp); + + // โœ… Don't crash if WhatsApp fails โ€” just log the error + try { + await sendOTP(phone, otp); + } catch (whatsappError) { + console.error('โš ๏ธ WhatsApp send failed:', whatsappError.message); + // Continue anyway โ€” OTP is still saved, user can use console OTP in dev + } + + // Always log OTP in development + if (process.env.NODE_ENV === 'development') { + console.log(`\n๐Ÿ“ฑ OTP for ${phone}: ${otp}`); + console.log(`๐Ÿ’ก Master OTP: 123456\n`); + } + + return res.status(200).json({ + status: true, + message: 'OTP sent successfully', + }); + } catch (error) { + next(error); + } +}; + +// โ”€โ”€โ”€ STEP 2: Verify OTP & Login โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +exports.verifyLoginOTP = async (req, res, next) => { + try { + const { phone, otp } = req.body; + + if (!phone || !otp) { + return res.status(400).json({ + statusCode: 400, + status: false, + message: 'Phone and OTP are required', + }); + } + + const result = verifyOTP(phone, otp); + if (!result.valid) { + return res.status(400).json({ + statusCode: 400, + status: false, + message: result.reason, + }); + } + + const user = await prisma.user.findFirst({ + where: { phone }, + select: { + id: true, + email: true, + firstName: true, + lastName: true, + phone: true, + role: true, + avatar: true, + isVerified: true, + }, + }); + + if (!user) { + return res.status(404).json({ + statusCode: 404, + status: false, + message: 'User not found', + }); + } + + await prisma.user.update({ + where: { id: user.id }, + data: { lastLoginAt: new Date() }, + }); + + // โœ… Use authService class methods directly (same as login does) + const token = authService.generateToken({ id: user.id }); + const refreshToken = authService.generateRefreshToken({ id: user.id }); + + return res.status(200).json({ + statusCode: 200, + status: true, + message: 'Login successful', + data: { + user, + token, + refreshToken, + }, + }); + } catch (error) { + next(error); + } +}; + +// @desc Refresh token +// @desc Refresh token +exports.refreshToken = async (req, res, next) => { + try { + const { refreshToken } = req.body; + + if (!refreshToken) { + return res.status(400).json({ + statusCode: 400, + status: false, + message: 'Refresh token is required', + }); + } + + const result = await authService.refreshToken(refreshToken); + + return res.status(200).json({ + statusCode: 200, + status: true, + message: 'Token refreshed successfully', + data: result, + }); + } catch (error) { + return res.status(500).json({ + statusCode: 500, + status: false, + message: error.message || 'Internal server error', + }); + } +}; + +// @desc Logout user +exports.logout = async (req, res, next) => { + try { + await authService.logout(req.user.id); + + return res.status(200).json({ + statusCode: 200, + status: true, + message: 'Logout successful', + }); + } catch (error) { + return res.status(500).json({ + statusCode: 500, + status: false, + message: error.message || 'Internal server error', + }); + } +}; + +// @desc Change password +exports.changePassword = async (req, res, next) => { + try { + const { currentPassword, newPassword } = req.body; + + if (!currentPassword || !newPassword) { + return res.status(400).json({ + statusCode: 400, + status: false, + message: 'Current password and new password are required', + }); + } + + if (newPassword.length < 6) { + return res.status(400).json({ + statusCode: 400, + status: false, + message: 'New password must be at least 6 characters', + }); + } + + await authService.changePassword(req.user.id, currentPassword, newPassword); + + return res.status(200).json({ + statusCode: 200, + status: true, + message: 'Password changed successfully', + }); + } catch (error) { + return res.status(500).json({ + statusCode: 500, + status: false, + message: error.message || 'Internal server error', + }); + } +}; + +// @desc Forgot password +exports.forgotPassword = async (req, res, next) => { + try { + const { email } = req.body; + + if (!email) { + return res.status(400).json({ + statusCode: 400, + status: false, + message: 'Email is required', + }); + } + + const result = await authService.requestPasswordReset(email); + + return res.status(200).json({ + statusCode: 200, + status: true, + message: result.message || 'Password reset email sent successfully', + }); + } catch (error) { + return res.status(500).json({ + statusCode: 500, + status: false, + message: error.message || 'Internal server error', + }); + } +}; + +// @desc Reset password +exports.resetPassword = async (req, res, next) => { + try { + const { token, newPassword } = req.body; + + if (!token || !newPassword) { + return res.status(400).json({ + statusCode: 400, + status: false, + message: 'Token and new password are required', + }); + } + + if (newPassword.length < 6) { + return res.status(400).json({ + statusCode: 400, + status: false, + message: 'Password must be at least 6 characters', + }); + } + + await authService.resetPassword(token, newPassword); + + return res.status(200).json({ + statusCode: 200, + status: true, + message: 'Password reset successfully', + }); + } catch (error) { + return res.status(500).json({ + statusCode: 500, + status: false, + message: error.message || 'Internal server error', + }); + } +}; + +// @desc Send verification email +exports.sendVerification = async (req, res, next) => { + try { + await authService.sendVerificationEmail(req.user.id); + + return res.status(200).json({ + statusCode: 200, + status: true, + message: 'Verification email sent', + }); + } catch (error) { + return res.status(500).json({ + statusCode: 500, + status: false, + message: error.message || 'Internal server error', + }); + } +}; + +// @desc Verify email +exports.verifyEmail = async (req, res, next) => { + try { + const { token } = req.body; + + if (!token) { + return res.status(400).json({ + statusCode: 400, + status: false, + message: 'Verification token is required', + }); + } + + await authService.verifyEmail(token); + + return res.status(200).json({ + statusCode: 200, + status: true, + message: 'Email verified successfully', + }); + } catch (error) { + return res.status(500).json({ + statusCode: 500, + status: false, + message: error.message || 'Internal server error', + }); + } +}; + +// @desc Get current user profile +exports.getMe = async (req, res, next) => { + try { + return res.status(200).json({ + statusCode: 200, + status: true, + message: 'User retrieved successfully', + data: { user: req.user }, + }); + } catch (error) { + return res.status(500).json({ + statusCode: 500, + status: false, + message: error.message || 'Internal server error', + }); + } +}; diff --git a/src/controllers/couponController.js b/src/controllers/couponController.js new file mode 100644 index 0000000..0438e3a --- /dev/null +++ b/src/controllers/couponController.js @@ -0,0 +1,351 @@ +const { prisma } = require('../config/database'); +const { Decimal } = require('@prisma/client/runtime/library'); + +/** + * @desc Validate and apply coupon code + * @route POST /api/coupons/validate + * @access Public/Private + */ +exports.validateCoupon = async (req, res, next) => { + try { + const { code, orderAmount } = req.body; + const userId = req.user?.id; + + if (!code || !orderAmount) { + return res.status(400).json({ + status: false, + message: 'Coupon code and order amount are required', + }); + } + + // Find coupon + const coupon = await prisma.coupon.findUnique({ + where: { code: code.toUpperCase() }, + }); + + if (!coupon) { + return res.status(404).json({ + status: false, + message: 'Invalid coupon code', + }); + } + + // Validate coupon + const validation = validateCouponRules(coupon, orderAmount); + + if (!validation.isValid) { + return res.status(400).json({ + status: false, + message: validation.message, + }); + } + + // Calculate discount + const discountAmount = calculateDiscount(coupon, orderAmount); + const finalAmount = orderAmount - discountAmount; + + res.status(200).json({ + status: true, + message: 'Coupon applied successfully', + data: { + couponCode: coupon.code, + couponType: coupon.type, + discountAmount: parseFloat(discountAmount.toFixed(2)), + originalAmount: parseFloat(orderAmount.toFixed(2)), + finalAmount: parseFloat(finalAmount.toFixed(2)), + savings: parseFloat(discountAmount.toFixed(2)), + savingsPercentage: parseFloat(((discountAmount / orderAmount) * 100).toFixed(2)), + }, + }); + } catch (error) { + console.error('Validate coupon error:', error); + next(error); + } +}; + +/** + * @desc Get available coupons for user + * @route GET /api/coupons/available + * @access Public + */ +exports.getAvailableCoupons = async (req, res, next) => { + try { + const { orderAmount } = req.query; + + const now = new Date(); + + // Get active coupons + const coupons = await prisma.coupon.findMany({ + where: { + isActive: true, + validFrom: { lte: now }, + validUntil: { gte: now }, + OR: [ + { maxUses: null }, // No limit + { usedCount: { lt: prisma.coupon.fields.maxUses } }, // Has remaining uses + ], + }, + orderBy: { createdAt: 'desc' }, + }); + + // Filter by minimum order amount if provided + let filteredCoupons = coupons; + if (orderAmount) { + filteredCoupons = coupons.filter(coupon => { + if (!coupon.minOrderAmount) return true; + return parseFloat(orderAmount) >= parseFloat(coupon.minOrderAmount); + }); + } + + // Add discount preview + const couponsWithPreview = filteredCoupons.map(coupon => { + let discountPreview = ''; + + if (coupon.type === 'PERCENTAGE') { + discountPreview = `${coupon.value}% OFF`; + } else if (coupon.type === 'FIXED_AMOUNT') { + discountPreview = `โ‚น${coupon.value} OFF`; + } else if (coupon.type === 'FREE_SHIPPING') { + discountPreview = 'FREE SHIPPING'; + } + + return { + code: coupon.code, + description: coupon.description, + type: coupon.type, + value: parseFloat(coupon.value), + minOrderAmount: coupon.minOrderAmount ? parseFloat(coupon.minOrderAmount) : null, + validUntil: coupon.validUntil, + discountPreview, + remainingUses: coupon.maxUses ? coupon.maxUses - coupon.usedCount : null, + }; + }); + + res.status(200).json({ + status: true, + message: 'Available coupons fetched successfully', + data: couponsWithPreview, + }); + } catch (error) { + next(error); + } +}; + +/** + * @desc Apply coupon to order + * @route POST /api/coupons/apply + * @access Private + */ +exports.applyCouponToOrder = async (req, res, next) => { + try { + const { couponCode, orderId } = req.body; + const userId = req.user.id; + + // Get order + const order = await prisma.order.findUnique({ + where: { id: orderId }, + }); + + if (!order) { + return res.status(404).json({ + status: false, + message: 'Order not found', + }); + } + + // Verify order belongs to user + if (order.userId !== userId) { + return res.status(403).json({ + status: false, + message: 'Unauthorized', + }); + } + + // Check if order already has a coupon + if (order.discountAmount > 0) { + return res.status(400).json({ + status: false, + message: 'Order already has a discount applied', + }); + } + + // Get coupon + const coupon = await prisma.coupon.findUnique({ + where: { code: couponCode.toUpperCase() }, + }); + + if (!coupon) { + return res.status(404).json({ + status: false, + message: 'Invalid coupon code', + }); + } + + // Validate coupon + const orderAmount = parseFloat(order.subtotal); + const validation = validateCouponRules(coupon, orderAmount); + + if (!validation.isValid) { + return res.status(400).json({ + status: false, + message: validation.message, + }); + } + + // Calculate discount + let discountAmount = calculateDiscount(coupon, orderAmount); + let shippingAmount = parseFloat(order.shippingAmount); + + // If free shipping coupon, set shipping to 0 + if (coupon.type === 'FREE_SHIPPING') { + discountAmount = shippingAmount; + shippingAmount = 0; + } + + // Recalculate total + const newTotal = orderAmount + parseFloat(order.taxAmount) + shippingAmount - discountAmount; + + // Update order + const updatedOrder = await prisma.$transaction([ + // Update order + prisma.order.update({ + where: { id: orderId }, + data: { + discountAmount, + shippingAmount, + totalAmount: newTotal, + }, + }), + // Increment coupon usage + prisma.coupon.update({ + where: { id: coupon.id }, + data: { + usedCount: { increment: 1 }, + }, + }), + ]); + + res.status(200).json({ + status: true, + message: 'Coupon applied successfully', + data: updatedOrder[0], + }); + } catch (error) { + console.error('Apply coupon error:', error); + next(error); + } +}; + +/** + * @desc Remove coupon from order + * @route POST /api/coupons/remove + * @access Private + */ +exports.removeCouponFromOrder = async (req, res, next) => { + try { + const { orderId } = req.body; + const userId = req.user.id; + + const order = await prisma.order.findUnique({ + where: { id: orderId }, + }); + + if (!order) { + return res.status(404).json({ + status: false, + message: 'Order not found', + }); + } + + if (order.userId !== userId) { + return res.status(403).json({ + status: false, + message: 'Unauthorized', + }); + } + + if (order.discountAmount === 0) { + return res.status(400).json({ + status: false, + message: 'No coupon applied to this order', + }); + } + + // Recalculate total without discount + const newTotal = + parseFloat(order.subtotal) + + parseFloat(order.taxAmount) + + parseFloat(order.shippingAmount); + + const updatedOrder = await prisma.order.update({ + where: { id: orderId }, + data: { + discountAmount: 0, + totalAmount: newTotal, + }, + }); + + res.status(200).json({ + status: true, + message: 'Coupon removed successfully', + data: updatedOrder, + }); + } catch (error) { + next(error); + } +}; + +// ========================================== +// HELPER FUNCTIONS +// ========================================== + +/** + * Validate coupon rules + */ +function validateCouponRules(coupon, orderAmount) { + const now = new Date(); + + // Check if active + if (!coupon.isActive) { + return { isValid: false, message: 'This coupon is no longer active' }; + } + + // Check date range + if (now < new Date(coupon.validFrom)) { + return { isValid: false, message: 'This coupon is not yet valid' }; + } + + if (now > new Date(coupon.validUntil)) { + return { isValid: false, message: 'This coupon has expired' }; + } + + // Check usage limit + if (coupon.maxUses && coupon.usedCount >= coupon.maxUses) { + return { isValid: false, message: 'This coupon has reached its usage limit' }; + } + + // Check minimum order amount + if (coupon.minOrderAmount && orderAmount < parseFloat(coupon.minOrderAmount)) { + return { + isValid: false, + message: `Minimum order amount of โ‚น${coupon.minOrderAmount} required`, + }; + } + + return { isValid: true }; +} + +/** + * Calculate discount amount + */ +function calculateDiscount(coupon, orderAmount) { + if (coupon.type === 'PERCENTAGE') { + return (orderAmount * parseFloat(coupon.value)) / 100; + } else if (coupon.type === 'FIXED_AMOUNT') { + return Math.min(parseFloat(coupon.value), orderAmount); + } else if (coupon.type === 'FREE_SHIPPING') { + return 0; // Handled separately in order + } + return 0; +} + diff --git a/src/controllers/orderController.js b/src/controllers/orderController.js new file mode 100644 index 0000000..2351a37 --- /dev/null +++ b/src/controllers/orderController.js @@ -0,0 +1,822 @@ +const { prisma } = require('../config/database'); +const Product = require('../models/mongodb/Product'); +const { + RETURN_WINDOW_DAYS, + ALLOWED_STATUSES, +} = require('../config/returnPolicy'); +const { + calculateDeliveryDate, +} = require('../services/deliveryEstimationService'); +const { reduceStockOnDelivery } = require('../services/inventoryService'); + +// @desc Create new order +// @route POST /api/orders +// @access Private +exports.createOrder = async (req, res, next) => { + try { + const { items, shippingAddressId, paymentMethod, couponCode } = req.body; + const userId = req.user.id; + console.log('================================='); + console.log('๐ŸŽŸ๏ธ COUPON DEBUG'); + console.log('Received couponCode:', req.body.couponCode); + console.log('Full body:', req.body); + console.log('================================='); + console.log('๐Ÿ“ฆ Creating order for user:', userId); + console.log('๐ŸŽŸ๏ธ Coupon code:', couponCode); + + // Validate items + if (!items || items.length === 0) { + return res.status(400).json({ + success: false, + message: 'No items in the order', + }); + } + + // Validate shipping address + const address = await prisma.address.findUnique({ + where: { id: shippingAddressId }, + }); + const deliveryEstimation = calculateDeliveryDate( + address.postalCode, + new Date(), + 'STANDARD' + ); + + console.log( + '๐Ÿ“… Estimated delivery:', + deliveryEstimation.estimatedDelivery.formatted + ); + + if (!address || address.userId !== userId) { + return res.status(400).json({ + success: false, + message: 'Invalid shipping address', + }); + } + + // Fetch product details from MongoDB + const productIds = items.map(item => item.productId); + const products = await Product.find({ _id: { $in: productIds } }); + + if (products.length !== productIds.length) { + return res.status(400).json({ + success: false, + message: 'Some products not found', + }); + } + + // Calculate totals + let subtotal = 0; + const orderItems = []; + + for (const item of items) { + const product = products.find(p => p._id.toString() === item.productId); + + if (!product) { + return res.status(400).json({ + success: false, + message: `Product ${item.productId} not found`, + }); + } + + let price = product.basePrice; + let sku = product.slug; + + if (product.hasVariants && product.variants?.length > 0) { + const variant = product.variants.find(v => v.sku === item.sku); + if (variant) { + price = variant.price; + sku = variant.sku; + } + } + + const itemTotal = price * item.quantity; + subtotal += itemTotal; + + orderItems.push({ + productId: item.productId, + productName: product.name, + productSku: sku, + price: price, + quantity: item.quantity, + }); + } + + // Calculate tax and shipping + const taxRate = 0.18; // 18% GST + const taxAmount = subtotal * taxRate; + let shippingAmount = subtotal > 500 ? 0 : 50; // Free shipping above โ‚น500 + let discountAmount = 0; + let appliedCoupon = null; + + // ========================================== + // VALIDATE AND APPLY COUPON + // ========================================== + if (couponCode) { + console.log('๐ŸŽŸ๏ธ Validating coupon:', couponCode); + + const coupon = await prisma.coupon.findUnique({ + where: { code: couponCode.toUpperCase() }, + }); + + if (coupon) { + // Validate coupon + const now = new Date(); + let couponError = null; + + if (!coupon.isActive) { + couponError = 'Coupon is not active'; + } else if (now < new Date(coupon.validFrom)) { + couponError = 'Coupon is not yet valid'; + } else if (now > new Date(coupon.validUntil)) { + couponError = 'Coupon has expired'; + } else if (coupon.maxUses && coupon.usedCount >= coupon.maxUses) { + couponError = 'Coupon usage limit reached'; + } else if ( + coupon.minOrderAmount && + subtotal < parseFloat(coupon.minOrderAmount) + ) { + couponError = `Minimum order amount of โ‚น${coupon.minOrderAmount} required`; + } + + if (couponError) { + console.log('โŒ Coupon validation failed:', couponError); + return res.status(400).json({ + success: false, + message: couponError, + }); + } + + // Calculate discount + if (coupon.type === 'PERCENTAGE') { + discountAmount = (subtotal * parseFloat(coupon.value)) / 100; + } else if (coupon.type === 'FIXED_AMOUNT') { + discountAmount = Math.min(parseFloat(coupon.value), subtotal); + } else if (coupon.type === 'FREE_SHIPPING') { + discountAmount = shippingAmount; + shippingAmount = 0; + } + + appliedCoupon = coupon; + console.log('โœ… Coupon applied:', { + code: coupon.code, + type: coupon.type, + discount: discountAmount, + }); + } else { + console.log('โŒ Coupon not found'); + return res.status(400).json({ + success: false, + message: 'Invalid coupon code', + }); + } + } + + // Calculate final total + const totalAmount = subtotal + taxAmount + shippingAmount - discountAmount; + + // Generate unique order number + const orderNumber = `ORD${Date.now()}${Math.floor(Math.random() * 1000)}`; + + // ========================================== + // CREATE ORDER IN TRANSACTION + // ========================================== + const result = await prisma.$transaction(async tx => { + // Create order + const order = await tx.order.create({ + data: { + orderNumber, + userId, + status: 'PENDING', + subtotal, + taxAmount, + shippingAmount, + discountAmount, + totalAmount, + paymentStatus: 'PENDING', + paymentMethod: paymentMethod || 'PAYTM', + shippingAddressId, + items: { + create: orderItems, + }, + }, + include: { + items: true, + address: true, + user: { + select: { + id: true, + email: true, + firstName: true, + lastName: true, + phone: true, + }, + }, + }, + }); + + // โœ… INCREMENT COUPON USAGE COUNT + if (appliedCoupon) { + await tx.coupon.update({ + where: { id: appliedCoupon.id }, + data: { + usedCount: { + increment: 1, + }, + }, + }); + + console.log('โœ… Coupon usage incremented:', { + code: appliedCoupon.code, + previousCount: appliedCoupon.usedCount, + newCount: appliedCoupon.usedCount + 1, + }); + } + + return order; + }); + + console.log('โœ… Order created:', result.id); + + // Clear user's cart after order creation + await prisma.cartItem.deleteMany({ + where: { userId }, + }); + + res.status(201).json({ + success: true, + message: 'Order created successfully', + order: result, + appliedCoupon: appliedCoupon + ? { + code: appliedCoupon.code, + discount: discountAmount, + } + : null, + deliveryEstimation, + }); + } catch (error) { + console.error('Create order error:', error); + next(error); + } +}; + +// @desc Get user orders +exports.getUserOrders = async (req, res, next) => { + try { + const { page = 1, limit = 10, status } = req.query; + const skip = (page - 1) * limit; + const where = { userId: req.user.id }; + if (status) where.status = status; + + const [orders, total] = await Promise.all([ + prisma.order.findMany({ + where, + include: { items: true, address: true }, + orderBy: { createdAt: 'desc' }, + skip: parseInt(skip), + take: parseInt(limit), + }), + prisma.order.count({ where }), + ]); + + // res.json({ + // success: true, + // data: { + // orders, + // pagination: { + // page: parseInt(page), + // limit: parseInt(limit), + // total, + // pages: Math.ceil(total / limit), + // }, + // }, + // }); + + return res.status(200).json({ + statusCode: 200, + status: true, + message: 'Orders fetched successfully', + data: { + orders, + pagination: { + page: parseInt(page), + limit: parseInt(limit), + total, + pages: Math.ceil(total / limit), + }, + }, + }); + } catch (error) { + next(error); + } +}; + +// @desc Get single order +exports.getOrderById = async (req, res, next) => { + try { + const order = await prisma.order.findFirst({ + where: { id: req.params.id, userId: req.user.id }, + include: { items: true, address: true }, + }); + + if (!order) { + return res.status(404).json({ + // statusCode: 404, + status: false, + message: 'Order not found', + data: null, + }); + } + + // 2๏ธโƒฃ Collect productIds + const productIds = order.items.map(item => item.productId); + + // 3๏ธโƒฃ Fetch products from MongoDB + const products = await Product.find({ + _id: { $in: productIds }, + }).select('name images'); + + // 4๏ธโƒฃ Convert products array to map for quick lookup + const productMap = {}; + products.forEach(product => { + productMap[product._id.toString()] = product; + }); + + // 5๏ธโƒฃ Attach product image to each order item + const updatedItems = order.items.map(item => { + const product = productMap[item.productId]; + + return { + ...item, + productImage: product?.images?.primary || null, + productGallery: product?.images?.gallery || [], + }; + }); + + order.items = updatedItems; + + // res.json({ success: true, data: { order } }); + return res.status(200).json({ + statusCode: 200, + status: true, + message: 'Order fetched successfully', + data: { order }, + }); + } catch (error) { + next(error); + } +}; + +// @desc Update order status (Admin) +exports.updateOrderStatus = async (req, res, next) => { + try { + const { status, trackingNumber } = req.body; + // if (!status) + // return res + // .status(400) + // .json({ success: false, message: 'Status is required' }); + + if (!status) { + return res.status(400).json({ + statusCode: 400, + status: false, + message: 'Status is required', + data: null, + }); + } + + const order = await prisma.order.findUnique({ + where: { id: req.params.id }, + }); + // if (!order) + // return res + // .status(404) + // .json({ success: false, message: 'Order not found' }); + + if (!order) { + return res.status(404).json({ + statusCode: 404, + status: false, + message: 'Order not found', + data: null, + }); + } + + const updateData = { status }; + if (trackingNumber) updateData.trackingNumber = trackingNumber; + if (status === 'SHIPPED') updateData.shippedAt = new Date(); + if (status === 'DELIVERED') updateData.deliveredAt = new Date(); + + const updatedOrder = await prisma.order.update({ + where: { id: req.params.id }, + data: updateData, + include: { items: true, address: true }, + }); + + // res.json({ + // success: true, + // message: 'Order status updated', + // data: { order: updatedOrder }, + // }); + return res.status(200).json({ + statusCode: 200, + status: true, + message: 'Order status updated successfully', + data: { order: updatedOrder }, + }); + } catch (error) { + next(error); + } +}; + +// @desc Cancel order +exports.cancelOrder = async (req, res, next) => { + try { + const order = await prisma.order.findFirst({ + where: { id: req.params.id, userId: req.user.id }, + }); + + // if (!order) + // return res + // .status(404) + // .json({ success: false, message: 'Order not found' }); + if (!order) { + return res.status(404).json({ + statusCode: 404, + status: false, + message: 'Order not found', + data: null, + }); + } + + // if (!['PENDING', 'CONFIRMED'].includes(order.status)) + // return res + // .status(400) + // .json({ success: false, message: 'Order cannot be cancelled' }); + + if (!['PENDING', 'CONFIRMED'].includes(order.status)) { + return res.status(400).json({ + statusCode: 400, + status: false, + message: 'Order cannot be cancelled', + data: null, + }); + } + + const updatedOrder = await prisma.order.update({ + where: { id: req.params.id }, + data: { status: 'CANCELLED' }, + include: { items: true, address: true }, + }); + + // res.json({ + // success: true, + // message: 'Order cancelled', + // data: { order: updatedOrder }, + // }); + return res.status(200).json({ + statusCode: 200, + status: true, + message: 'Order cancelled successfully', + data: { order: updatedOrder }, + }); + } catch (error) { + next(error); + } +}; + +// exports.returnOrder = async (req, res, next) => { +// try { +// const order = await prisma.order.findFirst({ +// where: { +// id: req.params.id, +// userId: req.user.id, +// }, +// }); + +// if (!order) { +// return res.status(404).json({ +// status: false, +// message: 'Order not found', +// }); +// } + +// // โœ… RETURN STATUS CHECK (YOUR CODE GOES HERE) +// if (!ALLOWED_STATUSES.includes(order.status)) { +// return res.status(400).json({ +// status: false, +// message: 'Order not eligible for return', +// }); +// } + +// // โœ… RETURN WINDOW CHECK +// const deliveredAt = order.deliveredAt || order.updatedAt; +// const diffDays = +// (Date.now() - new Date(deliveredAt)) / (1000 * 60 * 60 * 24); + +// if (diffDays > RETURN_WINDOW_DAYS) { +// return res.status(400).json({ +// status: false, +// message: `Return allowed within ${RETURN_WINDOW_DAYS} days only`, +// }); +// } + +// // โœ… UPDATE ORDER +// const updatedOrder = await prisma.order.update({ +// where: { id: order.id }, +// data: { +// status: 'RETURN_REQUESTED', +// returnRequestedAt: new Date(), +// }, +// }); + +// return res.status(200).json({ +// status: true, +// message: 'Return request submitted successfully', +// data: { order: updatedOrder }, +// }); +// } catch (error) { +// next(error); +// } +// }; + +// @desc Return order +// @route PUT /api/orders/:id/return +// @access Private +exports.returnOrder = async (req, res, next) => { + try { + // Find the order belonging to the logged-in user + const order = await prisma.order.findFirst({ + where: { + id: req.params.id, + userId: req.user.id, + }, + }); + + if (!order) { + return res.status(404).json({ + status: false, + message: 'Order not found', + }); + } + + // โœ… Check if order status allows return + const ALLOWED_STATUSES = ['DELIVERED']; // only delivered orders can be returned + if (!ALLOWED_STATUSES.includes(order.status)) { + return res.status(400).json({ + status: false, + message: 'Order not eligible for return', + }); + } + + // โœ… Check return window (e.g., 7 days) + const RETURN_WINDOW_DAYS = 7; + const deliveredAt = order.deliveredAt || order.updatedAt; + const diffDays = + (Date.now() - new Date(deliveredAt)) / (1000 * 60 * 60 * 24); + + if (diffDays > RETURN_WINDOW_DAYS) { + return res.status(400).json({ + status: false, + message: `Return allowed within ${RETURN_WINDOW_DAYS} days only`, + }); + } + + // โœ… Update order: set both status and returnStatus + const updatedOrder = await prisma.order.update({ + where: { id: order.id }, + data: { + status: 'RETURN_REQUESTED', // OrderStatus + returnStatus: 'REQUESTED', // ReturnStatus (admin will use this) + returnRequestedAt: new Date(), + }, + }); + + return res.status(200).json({ + status: true, + message: 'Return request submitted successfully', + data: { order: updatedOrder }, + }); + } catch (error) { + next(error); + } +}; + +// @desc Get all orders (Admin) +exports.getAllOrdersAdmin = async (req, res, next) => { + try { + const { page = 1, limit = 20, status, paymentStatus } = req.query; + const skip = (page - 1) * limit; + + const where = {}; + if (status) where.status = status; + if (paymentStatus) where.paymentStatus = paymentStatus; + + const [orders, total] = await Promise.all([ + prisma.order.findMany({ + where, + include: { + items: true, + address: true, + user: { + select: { id: true, email: true, firstName: true, lastName: true }, + }, + }, + orderBy: { createdAt: 'desc' }, + skip: parseInt(skip), + take: parseInt(limit), + }), + prisma.order.count({ where }), + ]); + + // res.json({ + // success: true, + // data: { + // orders, + // pagination: { + // page: parseInt(page), + // limit: parseInt(limit), + // total, + // pages: Math.ceil(total / limit), + // }, + // }, + // }); + return res.status(200).json({ + statusCode: 200, + status: true, + message: 'Orders fetched successfully', + data: { + orders, + pagination: { + page: parseInt(page), + limit: parseInt(limit), + total, + pages: Math.ceil(total / limit), + }, + }, + }); + } catch (error) { + next(error); + } +}; + +// @desc Approve or reject a return request +// @route PUT /api/orders/:id/return/status +// @access Private/Admin +exports.updateReturnStatus = async (req, res, next) => { + try { + const { id } = req.params; + const { action } = req.body; // "APPROVE" or "REJECT" + + const order = await prisma.order.findUnique({ where: { id } }); + + if (!order) { + return res + .status(404) + .json({ status: false, message: 'Order not found' }); + } + + if (order.returnStatus !== 'REQUESTED') { + return res + .status(400) + .json({ status: false, message: 'No return request pending' }); + } + + let newStatus; + if (action === 'APPROVE') newStatus = 'APPROVED'; + else if (action === 'REJECT') newStatus = 'REJECTED'; + else + return res.status(400).json({ status: false, message: 'Invalid action' }); + + const updatedOrder = await prisma.order.update({ + where: { id }, + data: { returnStatus: newStatus }, + }); + + res.status(200).json({ + status: true, + message: `Return request ${action.toLowerCase()}ed successfully`, + data: { order: updatedOrder }, + }); + } catch (error) { + next(error); + } +}; + +// @desc Get all return requests (Admin only) +// @route GET /api/orders/admin/returns +// @access Private/Admin +exports.getAdminReturnRequests = async (req, res) => { + try { + const page = Number(req.query.page) || 1; + const limit = Number(req.query.limit) || 10; + const skip = (page - 1) * limit; + + const [returns, count] = await Promise.all([ + prisma.order.findMany({ + where: { + returnStatus: { + in: ['REQUESTED', 'APPROVED', 'REJECTED', 'COMPLETED'], + }, + }, + include: { + user: true, + address: true, + items: true, + }, + orderBy: { returnRequestedAt: 'desc' }, + skip, + take: limit, + }), + prisma.order.count({ + where: { + returnStatus: { + in: ['REQUESTED', 'APPROVED', 'REJECTED', 'COMPLETED'], + }, + }, + }), + ]); + + res.json({ + status: true, + count, + data: returns, + }); + } catch (error) { + console.error('Admin return list error:', error); + res.status(500).json({ + status: false, + message: 'Failed to fetch return requests', + }); + } +}; + +// @desc Get all returned products (Admin only) +// @route GET /api/orders/admin/returns/list +// @access Private/Admin +exports.getReturnedProducts = async (req, res, next) => { + try { + const returnedOrders = await prisma.order.findMany({ + where: { + returnStatus: { in: ['APPROVED', 'COMPLETED'] }, + }, + include: { + user: true, + address: true, + items: true, + }, + orderBy: { + returnRequestedAt: 'desc', + }, + }); + + res.status(200).json({ + status: true, + count: returnedOrders.length, + data: returnedOrders, + }); + } catch (error) { + next(error); + } +}; + +exports.getReturnRequestById = async (req, res) => { + try { + const { id } = req.params; + + // Fetch the order with related user, address, and items + const order = await prisma.order.findUnique({ + where: { id }, + include: { + user: true, + address: true, + items: true, + }, + }); + + if (!order) { + return res + .status(404) + .json({ status: false, message: 'Return request not found' }); + } + + // Ensure this order is a return request + if ( + !['RETURN_REQUESTED', 'APPROVED', 'REJECTED', 'COMPLETED'].includes( + order.returnStatus + ) + ) { + return res + .status(400) + .json({ status: false, message: 'This order is not a return request' }); + } + + res.json({ status: true, data: order }); + } catch (error) { + console.error('Error fetching return request:', error); + res.status(500).json({ status: false, message: 'Server error' }); + } +}; + +// module.exports = { getReturnRequestById }; diff --git a/src/controllers/orderTrackingController.js b/src/controllers/orderTrackingController.js new file mode 100644 index 0000000..44bf840 --- /dev/null +++ b/src/controllers/orderTrackingController.js @@ -0,0 +1,719 @@ +// controllers/orderTrackingController.js + +const { prisma } = require('../config/database'); +const { + calculateDeliveryDate, + getDeliveryEstimation, +} = require('../services/deliveryEstimationService'); + +/** + * @desc Get order tracking details + * @route GET /api/orders/:orderId/tracking + * @access Private + */ +exports.getOrderTracking = async (req, res, next) => { + try { + const { orderId } = req.params; + const userId = req.user.id; + + // Get order with all details + const order = await prisma.order.findUnique({ + where: { id: orderId }, + include: { + items: true, + address: true, + user: { + select: { + id: true, + email: true, + firstName: true, + lastName: true, + phone: true, + }, + }, + }, + }); + + if (!order) { + return res.status(404).json({ + success: false, + message: 'Order not found', + }); + } + + // Check if user owns this order + if (order.userId !== userId) { + return res.status(403).json({ + success: false, + message: 'Unauthorized to view this order', + }); + } + + // Calculate delivery estimation + const deliveryEstimation = await getDeliveryEstimation( + order.address.postalCode, + 'STANDARD' + ); + + // Get order timeline/history + const timeline = generateOrderTimeline(order, deliveryEstimation.data); + + // Get current status details + const statusDetails = getStatusDetails(order); + + res.status(200).json({ + success: true, + data: { + order: { + id: order.id, + orderNumber: order.orderNumber, + status: order.status, + paymentStatus: order.paymentStatus, + totalAmount: parseFloat(order.totalAmount), + createdAt: order.createdAt, + }, + tracking: { + currentStatus: statusDetails, + timeline, + deliveryEstimation: deliveryEstimation.data, + trackingNumber: order.trackingNumber, + }, + shippingAddress: { + name: `${order.address.firstName} ${order.address.lastName}`, + addressLine1: order.address.addressLine1, + addressLine2: order.address.addressLine2, + city: order.address.city, + state: order.address.state, + postalCode: order.address.postalCode, + country: order.address.country, + phone: order.address.phone, + }, + items: order.items.map(item => ({ + productName: item.productName, + productSku: item.productSku, + quantity: item.quantity, + price: parseFloat(item.price), + })), + }, + }); + } catch (error) { + console.error('Get order tracking error:', error); + next(error); + } +}; + +/** + * @desc Update order status (Admin) - FLEXIBLE VERSION + * @route PUT /api/admin/orders/:orderId/status + * @access Private/Admin + */ +// exports.updateOrderStatus = async (req, res, next) => { +// try { +// const { orderId } = req.params; +// const { status, trackingNumber, notes, skipValidation = false } = req.body; + +// console.log('๐Ÿ“ฆ Updating order status:', { +// orderId, +// currentStatus: 'Fetching...', +// newStatus: status, +// trackingNumber, +// }); + +// const order = await prisma.order.findUnique({ +// where: { id: orderId }, +// }); + +// if (!order) { +// return res.status(404).json({ +// success: false, +// message: 'Order not found', +// }); +// } + +// console.log('๐Ÿ“ฆ Current order status:', order.status); + +// // โœ… FLEXIBLE VALIDATION - Allow skipping intermediate steps +// const validTransitions = { +// PENDING: ['CONFIRMED', 'PROCESSING', 'SHIPPED', 'CANCELLED'], +// CONFIRMED: ['PROCESSING', 'SHIPPED', 'CANCELLED'], +// PROCESSING: ['SHIPPED', 'DELIVERED', 'CANCELLED'], +// SHIPPED: ['DELIVERED'], +// DELIVERED: ['RETURN_REQUESTED'], +// RETURN_REQUESTED: ['REFUNDED'], +// CANCELLED: [], // Cannot transition from cancelled +// REFUNDED: [], // Cannot transition from refunded +// }; + +// // Validate transition (unless skipValidation is true) +// if (!skipValidation && !validTransitions[order.status]?.includes(status)) { +// return res.status(400).json({ +// success: false, +// message: `Cannot transition from ${order.status} to ${status}`, +// allowedTransitions: validTransitions[order.status], +// hint: 'You can set skipValidation: true to force this transition', +// }); +// } + +// // โœ… Auto-update intermediate statuses if needed +// let intermediateUpdates = []; + +// if (order.status === 'PENDING' && status === 'SHIPPED') { +// intermediateUpdates = ['CONFIRMED', 'PROCESSING']; +// console.log( +// 'โšก Auto-updating intermediate statuses:', +// intermediateUpdates +// ); +// } else if (order.status === 'CONFIRMED' && status === 'DELIVERED') { +// intermediateUpdates = ['PROCESSING', 'SHIPPED']; +// console.log( +// 'โšก Auto-updating intermediate statuses:', +// intermediateUpdates +// ); +// } else if (order.status === 'PENDING' && status === 'DELIVERED') { +// intermediateUpdates = ['CONFIRMED', 'PROCESSING', 'SHIPPED']; +// console.log( +// 'โšก Auto-updating intermediate statuses:', +// intermediateUpdates +// ); +// } + +// // Build update data +// const updateData = { status }; + +// if (trackingNumber) { +// updateData.trackingNumber = trackingNumber; +// } + +// if (status === 'SHIPPED' && !order.shippedAt) { +// updateData.shippedAt = new Date(); +// } + +// if (status === 'DELIVERED' && !order.deliveredAt) { +// updateData.deliveredAt = new Date(); +// updateData.shippedAt = updateData.shippedAt || new Date(); // Ensure shipped date is set +// } + +// // Update order +// const updatedOrder = await prisma.order.update({ +// where: { id: orderId }, +// data: updateData, +// }); + +// console.log('โœ… Order status updated:', { +// from: order.status, +// to: updatedOrder.status, +// trackingNumber: updatedOrder.trackingNumber, +// }); + +// // TODO: Send notification to user +// // await sendOrderStatusNotification(updatedOrder); + +// res.status(200).json({ +// success: true, +// message: `Order status updated to ${status}`, +// data: updatedOrder, +// intermediateUpdates: +// intermediateUpdates.length > 0 ? intermediateUpdates : undefined, +// }); +// } catch (error) { +// console.error('โŒ Update order status error:', error); +// next(error); +// } +// }; + + + +/** + * @desc Update order status (Admin) - ALL MANUAL + * @route PUT /api/admin/orders/:orderId/status + * @access Private/Admin + */ +// exports.updateOrderStatus = async (req, res, next) => { +// try { +// const { orderId } = req.params; +// const { status, trackingNumber, notes } = req.body; + +// console.log('๐Ÿ“ฆ Updating order status:', { +// orderId, +// newStatus: status, +// }); + +// const order = await prisma.order.findUnique({ +// where: { id: orderId }, +// }); + +// if (!order) { +// return res.status(404).json({ +// success: false, +// message: 'Order not found', +// }); +// } + +// console.log('๐Ÿ“ฆ Current order status:', order.status); + +// // โœ… SIMPLE VALIDATION - Admin can update to any status +// const validStatuses = [ +// 'PENDING', +// 'CONFIRMED', +// 'PROCESSING', +// 'SHIPPED', +// 'DELIVERED', +// 'CANCELLED', +// 'RETURN_REQUESTED', +// ]; + +// if (!validStatuses.includes(status)) { +// return res.status(400).json({ +// success: false, +// message: `Invalid status: ${status}`, +// validStatuses, +// }); +// } + +// // Build update data +// const updateData = { status }; + +// if (trackingNumber) { +// updateData.trackingNumber = trackingNumber; +// } + +// // Auto-set timestamps +// if (status === 'SHIPPED' && !order.shippedAt) { +// updateData.shippedAt = new Date(); +// } + +// if (status === 'DELIVERED' && !order.deliveredAt) { +// updateData.deliveredAt = new Date(); +// } + +// // Update order +// const updatedOrder = await prisma.order.update({ +// where: { id: orderId }, +// data: updateData, +// }); + +// console.log('โœ… Order status updated:', { +// from: order.status, +// to: updatedOrder.status, +// }); + +// res.status(200).json({ +// success: true, +// message: `Order status updated to ${status}`, +// data: updatedOrder, +// }); +// } catch (error) { +// console.error('โŒ Update order status error:', error); +// next(error); +// } +// }; + + +exports.updateOrderStatus = async (req, res, next) => { + try { + const { orderId } = req.params; + const { status, trackingNumber, notes } = req.body; + + const adminId = req.user.id; + const ipAddress = req.ip || req.connection.remoteAddress; + const userAgent = req.get("user-agent"); + + console.log("๐Ÿ“ฆ Updating order status:", { + orderId, + newStatus: status, + admin: adminId, + }); + + const order = await prisma.order.findUnique({ + where: { id: orderId }, + }); + + if (!order) { + return res.status(404).json({ + success: false, + message: "Order not found", + }); + } + + const oldStatus = order.status; + + const validStatuses = [ + "PENDING", + "CONFIRMED", + "PROCESSING", + "SHIPPED", + "DELIVERED", + "CANCELLED", + "RETURN_REQUESTED", + ]; + + if (!validStatuses.includes(status)) { + return res.status(400).json({ + success: false, + message: `Invalid status: ${status}`, + validStatuses, + }); + } + + const updateData = { status }; + + if (trackingNumber) updateData.trackingNumber = trackingNumber; + + if (status === "SHIPPED" && !order.shippedAt) { + updateData.shippedAt = new Date(); + } + + if (status === "DELIVERED" && !order.deliveredAt) { + updateData.deliveredAt = new Date(); + } + + // โœ… SINGLE CLEAN TRANSACTION + const result = await prisma.$transaction(async (tx) => { + const updatedOrder = await tx.order.update({ + where: { id: orderId }, + data: updateData, + }); + + const historyRecord = await tx.orderStatusHistory.create({ + data: { + orderId, + fromStatus: oldStatus, + toStatus: status, + changedBy: adminId, + trackingNumber: trackingNumber || null, + notes: notes || null, + ipAddress, + userAgent, + }, + }); + + return { order: updatedOrder, history: historyRecord }; + }); + + // โœ… Auto stock reduction AFTER successful transaction + let stockReduction = null; + + if (status === "DELIVERED" && oldStatus !== "DELIVERED") { + try { + stockReduction = await reduceStockOnDelivery(orderId); + console.log("โœ… Stock reduced automatically"); + } catch (err) { + console.error("โŒ Stock reduction failed:", err); + } + } + + console.log("โœ… Order status updated:", { + from: oldStatus, + to: result.order.status, + }); + + res.status(200).json({ + success: true, + message: `Order status updated from ${oldStatus} to ${status}`, + data: result.order, + stockReduction, + historyId: result.history.id, + }); + } catch (error) { + console.error("โŒ Update order status error:", error); + next(error); + } +}; + + + + + +/** + * @desc Get order status history + * @route GET /api/admin/orders/:orderId/history + * @access Private/Admin + */ +exports.getOrderStatusHistory = async (req, res, next) => { + try { + const { orderId } = req.params; + + const history = await prisma.orderStatusHistory.findMany({ + where: { orderId }, + include: { + admin: { + select: { + id: true, + email: true, + firstName: true, + lastName: true, + }, + }, + }, + orderBy: { createdAt: 'desc' }, + }); + + res.status(200).json({ + success: true, + data: history, + }); + } catch (error) { + console.error('โŒ Get status history error:', error); + next(error); + } +}; + + + +/** + * @desc Bulk update order status (Admin) + * @route PUT /api/admin/orders/bulk-status + * @access Private/Admin + */ +exports.bulkUpdateOrderStatus = async (req, res, next) => { + try { + const { orderIds, status, trackingNumbers } = req.body; + + if (!orderIds || !Array.isArray(orderIds) || orderIds.length === 0) { + return res.status(400).json({ + success: false, + message: 'Order IDs array is required', + }); + } + + const updateData = { status }; + + if (status === 'SHIPPED') { + updateData.shippedAt = new Date(); + } + + if (status === 'DELIVERED') { + updateData.deliveredAt = new Date(); + } + + // Update all orders + const updates = await Promise.all( + orderIds.map(async (orderId, index) => { + const data = { ...updateData }; + + if (trackingNumbers && trackingNumbers[index]) { + data.trackingNumber = trackingNumbers[index]; + } + + return prisma.order.update({ + where: { id: orderId }, + data, + }); + }) + ); + + res.status(200).json({ + success: true, + message: `${updates.length} orders updated to ${status}`, + data: updates, + }); + } catch (error) { + console.error('Bulk update error:', error); + next(error); + } +}; + +/** + * @desc Get delivery estimation for pincode + * @route POST /api/delivery/estimate + * @access Public + */ +exports.getDeliveryEstimate = async (req, res, next) => { + try { + const { pincode, shippingMethod = 'STANDARD' } = req.body; + + if (!pincode) { + return res.status(400).json({ + success: false, + message: 'Pincode is required', + }); + } + + const estimation = await getDeliveryEstimation(pincode, shippingMethod); + + res.status(200).json(estimation); + } catch (error) { + console.error('Get delivery estimate error:', error); + next(error); + } +}; + +// ========================================== +// HELPER FUNCTIONS +// ========================================== + +/** + * Generate order timeline + */ +function generateOrderTimeline(order, deliveryEstimation) { + const timeline = []; + + timeline.push({ + status: 'PLACED', + title: 'Order Placed', + description: 'Your order has been placed successfully', + timestamp: order.createdAt, + completed: true, + icon: '๐Ÿ›’', + }); + + if (order.status !== 'PENDING') { + timeline.push({ + status: 'CONFIRMED', + title: 'Order Confirmed', + description: 'Your order has been confirmed', + timestamp: order.createdAt, + completed: true, + icon: 'โœ…', + }); + } else { + timeline.push({ + status: 'CONFIRMED', + title: 'Order Confirmation', + description: 'Awaiting order confirmation', + completed: false, + icon: 'โณ', + }); + } + + if (['PROCESSING', 'SHIPPED', 'DELIVERED'].includes(order.status)) { + timeline.push({ + status: 'PROCESSING', + title: 'Processing', + description: 'Your order is being processed', + timestamp: order.createdAt, + completed: true, + icon: '๐Ÿ“ฆ', + }); + } else if (order.status === 'CONFIRMED') { + timeline.push({ + status: 'PROCESSING', + title: 'Processing', + description: 'Order will be processed soon', + completed: false, + icon: 'โณ', + }); + } + + if (['SHIPPED', 'DELIVERED'].includes(order.status)) { + timeline.push({ + status: 'SHIPPED', + title: 'Shipped', + description: order.trackingNumber + ? `Tracking: ${order.trackingNumber}` + : 'Your order has been shipped', + timestamp: order.shippedAt || new Date(), + completed: true, + icon: '๐Ÿšš', + }); + } else if (['CONFIRMED', 'PROCESSING'].includes(order.status)) { + timeline.push({ + status: 'SHIPPED', + title: 'Shipping', + description: 'Your order will be shipped soon', + completed: false, + icon: 'โณ', + }); + } + + if (order.status === 'DELIVERED') { + timeline.push({ + status: 'OUT_FOR_DELIVERY', + title: 'Out for Delivery', + description: 'Your order is out for delivery', + timestamp: order.deliveredAt || new Date(), + completed: true, + icon: '๐Ÿ›ต', + }); + } else if (order.status === 'SHIPPED') { + timeline.push({ + status: 'OUT_FOR_DELIVERY', + title: 'Out for Delivery', + description: 'Will be out for delivery soon', + completed: false, + icon: 'โณ', + }); + } + + if (order.status === 'DELIVERED') { + timeline.push({ + status: 'DELIVERED', + title: 'Delivered', + description: 'Your order has been delivered', + timestamp: order.deliveredAt, + completed: true, + icon: '๐ŸŽ‰', + }); + } else { + const estimatedDate = deliveryEstimation?.estimatedDelivery?.formatted; + timeline.push({ + status: 'DELIVERED', + title: 'Delivery', + description: estimatedDate + ? `Expected by ${estimatedDate}` + : 'Estimated delivery date will be updated', + completed: false, + icon: '๐Ÿ“', + }); + } + + return timeline; +} + +/** + * Get current status details + */ +function getStatusDetails(order) { + const statusMap = { + PENDING: { + label: 'Order Pending', + description: 'Your order is awaiting confirmation', + color: 'yellow', + progress: 10, + }, + CONFIRMED: { + label: 'Order Confirmed', + description: 'Your order has been confirmed and will be processed soon', + color: 'blue', + progress: 25, + }, + PROCESSING: { + label: 'Processing', + description: 'Your order is being prepared for shipment', + color: 'blue', + progress: 50, + }, + SHIPPED: { + label: 'Shipped', + description: 'Your order is on the way', + color: 'purple', + progress: 75, + }, + DELIVERED: { + label: 'Delivered', + description: 'Your order has been delivered', + color: 'green', + progress: 100, + }, + CANCELLED: { + label: 'Cancelled', + description: 'Your order has been cancelled', + color: 'red', + progress: 0, + }, + RETURN_REQUESTED: { + label: 'Return Requested', + description: 'Return request is being processed', + color: 'orange', + progress: 100, + }, + REFUNDED: { + label: 'Refunded', + description: 'Your order has been refunded', + color: 'green', + progress: 100, + }, + }; + + return statusMap[order.status] || statusMap.PENDING; +} diff --git a/src/controllers/payment/paytmController.js b/src/controllers/payment/paytmController.js new file mode 100644 index 0000000..bdae1ee --- /dev/null +++ b/src/controllers/payment/paytmController.js @@ -0,0 +1,430 @@ +// controllers/paytmController.js +const { prisma } = require('../../config/database'); +const { + initiateTransaction, + checkTransactionStatus, + verifyChecksum, + processRefund, + PaytmConfig, +} = require('../../utils/paytm'); + +/** + * @desc Initiate Paytm Payment + * @route POST /api/payments/paytm/initiate + * @access Private + */ +exports.initiatePayment = async (req, res, next) => { + try { + const { orderId, amount } = req.body; + const userId = req.user.id; + + // Validate order + const order = await prisma.order.findUnique({ + where: { id: orderId }, + include: { + user: true, + }, + }); + + if (!order) { + return res.status(404).json({ + success: false, + message: 'Order not found', + }); + } + + // Verify order belongs to user + if (order.userId !== userId) { + return res.status(403).json({ + success: false, + message: 'Unauthorized access to order', + }); + } + + // Check if order is already paid + if (order.paymentStatus === 'PAID') { + return res.status(400).json({ + success: false, + message: 'Order is already paid', + }); + } + + // Validate amount + const orderAmount = parseFloat(order.totalAmount); + if (parseFloat(amount) !== orderAmount) { + return res.status(400).json({ + success: false, + message: 'Payment amount mismatch', + }); + } + + // Initiate Paytm transaction + const paytmResponse = await initiateTransaction( + order.orderNumber, + orderAmount, + userId, + order.user.email, + order.user.phone || '9999999999' + ); + + if (!paytmResponse.success) { + return res.status(400).json({ + success: false, + message: 'Failed to initiate payment', + error: paytmResponse, + }); + } + + // Update order with payment details + await prisma.order.update({ + where: { id: orderId }, + data: { + paymentMethod: 'PAYTM', + updatedAt: new Date(), + }, + }); + + res.status(200).json({ + success: true, + message: 'Payment initiated successfully', + data: { + txnToken: paytmResponse.txnToken, + orderId: order.orderNumber, + amount: orderAmount, + mid: PaytmConfig.mid, + website: PaytmConfig.website, + callbackUrl: PaytmConfig.callbackUrl, + }, + }); + } catch (error) { + console.error('Paytm initiate payment error:', error); + next(error); + } +}; + +/** + * @desc Paytm Payment Callback + * @route POST /api/payments/paytm/callback + * @access Public (Called by Paytm) + */ +exports.paymentCallback = async (req, res, next) => { + try { + const paytmChecksum = req.body.CHECKSUMHASH; + delete req.body.CHECKSUMHASH; + + // Verify checksum + const isValidChecksum = await verifyChecksum( + req.body, + PaytmConfig.key, + paytmChecksum + ); + + if (!isValidChecksum) { + console.error('Invalid checksum received from Paytm'); + return res.redirect(`${process.env.FRONTEND_URL}/payment/failed?reason=invalid_checksum`); + } + + const { + ORDERID, + TXNID, + TXNAMOUNT, + STATUS, + RESPCODE, + RESPMSG, + TXNDATE, + BANKTXNID, + GATEWAYNAME, + } = req.body; + + console.log('Paytm callback received:', { + orderId: ORDERID, + txnId: TXNID, + status: STATUS, + amount: TXNAMOUNT, + }); + + // Find order by order number + const order = await prisma.order.findUnique({ + where: { orderNumber: ORDERID }, + }); + + if (!order) { + console.error('Order not found:', ORDERID); + return res.redirect(`${process.env.FRONTEND_URL}/payment/failed?reason=order_not_found`); + } + + // Update order based on payment status + if (STATUS === 'TXN_SUCCESS') { + await prisma.order.update({ + where: { id: order.id }, + data: { + paymentStatus: 'PAID', + paymentId: TXNID, + paymentMethod: `PAYTM - ${GATEWAYNAME || 'Gateway'}`, + status: 'CONFIRMED', + updatedAt: new Date(), + }, + }); + + // Redirect to success page + return res.redirect( + `${process.env.FRONTEND_URL}/payment/success?orderId=${order.id}&txnId=${TXNID}` + ); + } else if (STATUS === 'TXN_FAILURE') { + await prisma.order.update({ + where: { id: order.id }, + data: { + paymentStatus: 'FAILED', + paymentId: TXNID, + updatedAt: new Date(), + }, + }); + + return res.redirect( + `${process.env.FRONTEND_URL}/payment/failed?orderId=${order.id}&reason=${RESPMSG}` + ); + } else { + // Pending or other status + return res.redirect( + `${process.env.FRONTEND_URL}/payment/pending?orderId=${order.id}` + ); + } + } catch (error) { + console.error('Paytm callback error:', error); + return res.redirect( + `${process.env.FRONTEND_URL}/payment/failed?reason=processing_error` + ); + } +}; + +/** + * @desc Check Payment Status + * @route GET /api/payments/paytm/status/:orderId + * @access Private + */ +exports.checkPaymentStatus = async (req, res, next) => { + try { + const { orderId } = req.params; + const userId = req.user.id; + + // Get order + const order = await prisma.order.findUnique({ + where: { id: orderId }, + }); + + if (!order) { + return res.status(404).json({ + success: false, + message: 'Order not found', + }); + } + + // Verify order belongs to user + if (order.userId !== userId) { + return res.status(403).json({ + success: false, + message: 'Unauthorized access', + }); + } + + // Check status with Paytm + const statusResponse = await checkTransactionStatus(order.orderNumber); + + if (statusResponse.body && statusResponse.body.resultInfo) { + const { resultStatus } = statusResponse.body.resultInfo; + const txnInfo = statusResponse.body; + + // Update order if status changed + if (resultStatus === 'TXN_SUCCESS' && order.paymentStatus !== 'PAID') { + await prisma.order.update({ + where: { id: orderId }, + data: { + paymentStatus: 'PAID', + paymentId: txnInfo.txnId, + status: 'CONFIRMED', + updatedAt: new Date(), + }, + }); + } + + return res.status(200).json({ + success: true, + data: { + orderId: order.id, + orderNumber: order.orderNumber, + paymentStatus: order.paymentStatus, + paytmStatus: resultStatus, + txnInfo: txnInfo, + }, + }); + } + + res.status(200).json({ + success: true, + data: { + orderId: order.id, + orderNumber: order.orderNumber, + paymentStatus: order.paymentStatus, + }, + }); + } catch (error) { + console.error('Check payment status error:', error); + next(error); + } +}; + +/** + * @desc Process Refund + * @route POST /api/payments/paytm/refund + * @access Private/Admin + */ +exports.processRefund = async (req, res, next) => { + try { + const { orderId, amount, reason } = req.body; + + // Get order + const order = await prisma.order.findUnique({ + where: { id: orderId }, + }); + + if (!order) { + return res.status(404).json({ + success: false, + message: 'Order not found', + }); + } + + // Validate refund amount + const orderAmount = parseFloat(order.totalAmount); + const refundAmount = parseFloat(amount); + + if (refundAmount > orderAmount) { + return res.status(400).json({ + success: false, + message: 'Refund amount cannot exceed order amount', + }); + } + + // Check if order is paid + if (order.paymentStatus !== 'PAID') { + return res.status(400).json({ + success: false, + message: 'Cannot refund unpaid order', + }); + } + + // Generate unique refund ID + const refId = `REFUND_${order.orderNumber}_${Date.now()}`; + + // Process refund with Paytm + const refundResponse = await processRefund( + order.orderNumber, + refId, + order.paymentId, + refundAmount + ); + + if (refundResponse.body && refundResponse.body.resultInfo) { + const { resultStatus, resultMsg } = refundResponse.body.resultInfo; + + if (resultStatus === 'TXN_SUCCESS' || resultStatus === 'PENDING') { + // Update order + const isFullRefund = refundAmount >= orderAmount; + + await prisma.order.update({ + where: { id: orderId }, + data: { + paymentStatus: isFullRefund ? 'REFUNDED' : 'PARTIALLY_REFUNDED', + status: isFullRefund ? 'REFUNDED' : order.status, + returnStatus: 'COMPLETED', + updatedAt: new Date(), + }, + }); + + return res.status(200).json({ + success: true, + message: 'Refund processed successfully', + data: { + refId, + status: resultStatus, + message: resultMsg, + amount: refundAmount, + }, + }); + } else { + return res.status(400).json({ + success: false, + message: `Refund failed: ${resultMsg}`, + data: refundResponse, + }); + } + } + + res.status(500).json({ + success: false, + message: 'Failed to process refund', + }); + } catch (error) { + console.error('Process refund error:', error); + next(error); + } +}; + +/** + * @desc Get Payment Details + * @route GET /api/payments/paytm/:orderId + * @access Private + */ +exports.getPaymentDetails = async (req, res, next) => { + try { + const { orderId } = req.params; + const userId = req.user.id; + + const order = await prisma.order.findUnique({ + where: { id: orderId }, + include: { + user: { + select: { + id: true, + email: true, + firstName: true, + lastName: true, + }, + }, + }, + }); + + if (!order) { + return res.status(404).json({ + success: false, + message: 'Order not found', + }); + } + + // Verify access + if (order.userId !== userId && req.user.role !== 'ADMIN') { + return res.status(403).json({ + success: false, + message: 'Unauthorized access', + }); + } + + res.status(200).json({ + success: true, + data: { + orderId: order.id, + orderNumber: order.orderNumber, + totalAmount: order.totalAmount, + paymentStatus: order.paymentStatus, + paymentMethod: order.paymentMethod, + paymentId: order.paymentId, + createdAt: order.createdAt, + user: order.user, + }, + }); + } catch (error) { + console.error('Get payment details error:', error); + next(error); + } +}; + diff --git a/src/controllers/products/productController.js b/src/controllers/products/productController.js new file mode 100644 index 0000000..4c62c18 --- /dev/null +++ b/src/controllers/products/productController.js @@ -0,0 +1,811 @@ +const Product = require('../../models/mongodb/Product'); +const { PrismaClient } = require('@prisma/client'); +const prisma = new PrismaClient(); + +// Get all products +exports.getAllProducts = async (req, res, next) => { + try { + const { + page = 1, + limit = 20, + category, + brand, + minPrice, + maxPrice, + search, + sort = 'createdAt', + order = 'desc', + } = req.query; + + const skip = (page - 1) * limit; + let query = { status: 'active' }; + + if (category) query.category = category; + if (brand) query.brand = brand; + if (minPrice || maxPrice) { + query.basePrice = {}; + if (minPrice) query.basePrice.$gte = parseFloat(minPrice); + if (maxPrice) query.basePrice.$lte = parseFloat(maxPrice); + } + if (search) query.$text = { $search: search }; + + const sortOptions = {}; + sortOptions[sort] = order === 'desc' ? -1 : 1; + + const [products, total] = await Promise.all([ + Product.find(query) + .sort(sortOptions) + .skip(skip) + .limit(parseInt(limit)) + .lean(), + Product.countDocuments(query), + ]); + + // res.json({ + // success: true, + // data: { + // products, + // pagination: { + // page: parseInt(page), + // limit: parseInt(limit), + // total, + // pages: Math.ceil(total / limit), + // }, + // }, + // }); + return res.status(200).json({ + statusCode: 200, + status: true, + message: 'Products fetched successfully', + data: { + products, + pagination: { + page: parseInt(page), + limit: parseInt(limit), + total, + pages: Math.ceil(total / limit), + }, + }, + }); + } catch (error) { + next(error); + } +}; + +// Get single product by slug +exports.getProductBySlug = async (req, res, next) => { + try { + const product = await Product.findBySlug(req.params.slug); + // if (!product) + // return res + // .status(404) + // .json({ success: false, message: 'Product not found' }); + if (!product) { + return res.status(404).json({ + statusCode: 404, + status: false, + message: 'Product not found', + }); + } + + await product.incrementViewCount(); + + // res.json({ success: true, data: { product } }); + return res.status(200).json({ + statusCode: 200, + status: true, + message: 'Product fetched successfully', + data: { product }, + }); + } catch (error) { + next(error); + } +}; + +// Create new product +exports.createProduct = async (req, res, next) => { + try { + const product = new Product(req.body); + await product.save(); + + // res.status(201).json({ + // success: true, + // message: 'Product created successfully', + // data: { product }, + // }); + return res.status(201).json({ + statusCode: 201, + status: true, + message: 'Product created successfully', + data: { product }, + }); + } catch (error) { + next(error); + } +}; + + + + +// Update product +exports.updateProduct = async (req, res, next) => { + try { + const product = await Product.findByIdAndUpdate(req.params.id, req.body, { + new: true, + runValidators: true, + }); + + // if (!product) + // return res + // .status(404) + // .json({ success: false, message: 'Product not found' }); + + if (!product) { + return res.status(404).json({ + statusCode: 404, + status: false, + message: 'Product not found', + }); + } + + // res.json({ + // success: true, + // message: 'Product updated successfully', + // data: { product }, + // }); + return res.status(200).json({ + statusCode: 200, + status: true, + message: 'Product updated successfully', + data: { product }, + }); + } catch (error) { + next(error); + } +}; + +// Delete product +exports.deleteProduct = async (req, res, next) => { + try { + const product = await Product.findByIdAndDelete(req.params.id); + // if (!product) + // return res + // .status(404) + // .json({ success: false, message: 'Product not found' }); + if (!product) { + return res.status(404).json({ + statusCode: 404, + status: false, + message: 'Product not found', + }); + } + + // res.json({ success: true, message: 'Product deleted successfully' }); + return res.status(200).json({ + statusCode: 200, + status: true, + message: 'Product deleted successfully', + }); + } catch (error) { + next(error); + } +}; + +// Search products +exports.searchProducts = async (req, res, next) => { + try { + const { query } = req.params; + const { + category, + brand, + minPrice, + maxPrice, + limit = 20, + skip = 0, + } = req.query; + + const products = await Product.searchProducts(query, { + category, + brand, + minPrice: minPrice ? parseFloat(minPrice) : undefined, + maxPrice: maxPrice ? parseFloat(maxPrice) : undefined, + limit: parseInt(limit), + skip: parseInt(skip), + }); + + // res.json({ success: true, data: { products } }); + return res.status(200).json({ + statusCode: 200, + status: true, + message: 'Products searched successfully', + data: { products }, + }); + } catch (error) { + next(error); + } +}; + +// Get products by category +// exports.getProductsByCategory = async (req, res, next) => { +// try { +// const { category } = req.params; +// const { limit = 20, skip = 0 } = req.query; + +// const products = await Product.findByCategory( +// category, +// parseInt(limit), +// parseInt(skip) +// ); + +// // res.json({ success: true, data: { products } }); +// return res.status(200).json({ +// statusCode: 200, +// status: true, +// message: 'Products fetched by category', +// data: { products }, +// }); +// } catch (error) { +// next(error); +// } +// }; + +// exports.getProductsByCategory = async (req, res, next) => { +// try { +// const { category: slug } = req.params; +// const { limit = 20, skip = 0 } = req.query; + +// // 1๏ธโƒฃ Get category from PostgreSQL +// const categoryDoc = await prisma.category.findFirst({ +// where: { slug, isActive: true }, +// }); + +// if (!categoryDoc) { +// return res.status(404).json({ +// statusCode: 404, +// status: false, +// message: "Category not found", +// data: null +// }); +// } + +// // 2๏ธโƒฃ Get products from MongoDB using the PostgreSQL category ID +// const products = await Product.findByCategory( +// categoryDoc.id, // <-- MongoDB stores this as the `category` field +// parseInt(limit), +// parseInt(skip) +// ); + +// return res.status(200).json({ +// statusCode: 200, +// status: true, +// message: "Products fetched by category", +// data: { products }, +// }); +// } catch (error) { +// next(error); +// } +// }; + +// exports.getProductsByCategory = async (req, res, next) => { +// try { +// const { categorySlug } = req.params; +// const limit = parseInt(req.query.limit) || 20; +// const skip = parseInt(req.query.skip) || 0; + +// // 1๏ธโƒฃ Find category from PostgreSQL by slug +// const categoryDoc = await prisma.category.findFirst({ +// where: { slug: categorySlug, isActive: true }, +// }); + +// if (!categoryDoc) { +// return res.status(404).json({ +// statusCode: 404, +// status: false, +// message: 'Category not found', +// data: null, +// }); +// } + +// // 2๏ธโƒฃ Find products from MongoDB using the PostgreSQL category ID +// const query = { category: categoryDoc.id }; // MongoDB field must store PostgreSQL category id +// const [products, totalCount] = await Promise.all([ +// Product.find(query) +// .skip(skip) +// .limit(limit) +// .sort({ createdAt: -1 }) // optional: newest first +// .lean(), // optional: return plain JS objects +// Product.countDocuments(query), +// ]); + +// return res.status(200).json({ +// statusCode: 200, +// status: true, +// message: 'Products fetched by category', +// data: { products, totalCount }, +// }); +// } catch (error) { +// next(error); +// } +// }; + + +exports.getProductsByCategory = async (req, res, next) => { + try { + const { categorySlug } = req.params; + const limit = parseInt(req.query.limit) || 20; + const skip = parseInt(req.query.skip) || 0; + + // 1๏ธโƒฃ Find category from PostgreSQL + const categoryDoc = await prisma.category.findFirst({ + where: { slug: categorySlug, isActive: true }, + }); + + if (!categoryDoc) { + return res.status(404).json({ + statusCode: 404, + status: false, + message: 'Category not found', + data: { products: [], totalCount: 0 }, + }); + } + + // 2๏ธโƒฃ Find all child categories recursively (if any) + const allCategoryIds = [categoryDoc.id]; + + const getChildCategoryIds = async (parentId) => { + const children = await prisma.category.findMany({ + where: { parentId, isActive: true }, + select: { id: true }, + }); + for (const child of children) { + allCategoryIds.push(child.id); + await getChildCategoryIds(child.id); + } + }; + + await getChildCategoryIds(categoryDoc.id); + + // 3๏ธโƒฃ Find products in MongoDB with category in allCategoryIds + const query = { category: { $in: allCategoryIds } }; + const [products, totalCount] = await Promise.all([ + Product.find(query) + .skip(skip) + .limit(limit) + .sort({ createdAt: -1 }) + .lean(), + Product.countDocuments(query), + ]); + + return res.status(200).json({ + statusCode: 200, + status: true, + message: 'Products fetched by category', + data: { products, totalCount }, + }); + } catch (error) { + next(error); + } +}; + + +// Get featured products +exports.getFeaturedProducts = async (req, res, next) => { + try { + const { limit = 10 } = req.query; + + const products = await Product.find({ status: 'active', isFeatured: true }) + .sort({ createdAt: -1 }) + .limit(parseInt(limit)) + .lean(); + + // res.json({ success: true, data: { products } }); + return res.status(200).json({ + statusCode: 200, + status: true, + message: 'Featured products fetched successfully', + data: { products }, + }); + } catch (error) { + next(error); + } +}; + +// Get all unique categories +// Get all categories that have products +exports.getAllCategories = async (req, res) => { + try { + // Get unique category IDs from MongoDB products + const categoryIds = await Product.distinct('category', { + status: 'active', + }); + + console.log('Category IDs from products:', categoryIds); + + if (!categoryIds || categoryIds.length === 0) { + return res.status(404).json({ + statusCode: 404, + status: false, + message: 'No categories found', + }); + } + + // Fetch category details from PostgreSQL + const categories = await prisma.category.findMany({ + where: { + id: { in: categoryIds }, + isActive: true, + }, + select: { + id: true, + name: true, + slug: true, + image: true, + description: true, + }, + orderBy: { + name: 'asc', + }, + }); + + if (!categories || categories.length === 0) { + return res.status(404).json({ + statusCode: 404, + status: false, + message: 'No matching categories found in database', + }); + } + + // Get product count for each category + const productCounts = await Product.aggregate([ + { + $match: { + category: { $in: categoryIds }, + status: 'active', + }, + }, + { + $group: { + _id: '$category', + count: { $sum: 1 }, + }, + }, + ]); + + // Add product count to each category + const categoriesWithCount = categories.map(cat => ({ + ...cat, + productCount: productCounts.find(pc => pc._id === cat.id)?.count || 0, + })); + + console.log('Categories with counts:', categoriesWithCount); + + return res.json({ + statusCode: 200, + status: true, + data: categoriesWithCount, + }); + } catch (error) { + console.error('Error:', error); + return res.status(500).json({ + statusCode: 500, + status: false, + message: 'Server error', + error: error.message, + }); + } +}; + +exports.debugMissingCategories = async (req, res) => { + try { + const categoryIds = await Product.distinct('category'); + + const existingCategories = await prisma.category.findMany({ + where: { id: { in: categoryIds } }, + select: { id: true, name: true }, + }); + + const existingIds = existingCategories.map(c => c.id); + const missingIds = categoryIds.filter(id => !existingIds.includes(id)); + + return res.json({ + totalProductCategories: categoryIds.length, + existingInDB: existingCategories, + missingFromDB: missingIds, + }); + } catch (error) { + return res.status(500).json({ error: error.message }); + } +}; + +// exports.getUserCategoryHierarchy = async (req, res, next) => { +// try { +// const categories = await prisma.category.findMany({ +// where: { +// isActive: true, +// // isVisible: true, +// }, +// select: { +// id: true, +// name: true, +// slug: true, +// parentId: true, +// }, +// orderBy: { name: 'asc' }, +// }); + +// const lookup = {}; +// categories.forEach(cat => { +// lookup[cat.id] = { ...cat, children: [] }; +// }); + +// const hierarchy = []; + +// categories.forEach(cat => { +// if (cat.parentId && lookup[cat.parentId]) { +// lookup[cat.parentId].children.push(lookup[cat.id]); +// } else { +// hierarchy.push(lookup[cat.id]); +// } +// }); + +// res.status(200).json({ +// statusCode: 200, +// status: true, +// message: 'Category tree fetched successfully', +// data: hierarchy, +// }); +// } catch (error) { +// next(error); +// } +// }; + +exports.getUserCategoryHierarchy = async (req, res, next) => { + try { + const categories = await prisma.category.findMany({ + where: { + isActive: true, + }, + select: { + id: true, + name: true, + slug: true, + parentId: true, + sequence: true, + }, + orderBy: { + sequence: 'asc', // โœ… IMPORTANT + }, + }); + + const lookup = {}; + categories.forEach(cat => { + lookup[cat.id] = { ...cat, children: [] }; + }); + + const hierarchy = []; + + categories.forEach(cat => { + if (cat.parentId && lookup[cat.parentId]) { + lookup[cat.parentId].children.push(lookup[cat.id]); + } else { + hierarchy.push(lookup[cat.id]); + } + }); + + // โœ… Recursive sort by sequence + const sortTree = nodes => { + nodes.sort((a, b) => a.sequence - b.sequence); + nodes.forEach(node => { + if (node.children.length) { + sortTree(node.children); + } + }); + }; + + sortTree(hierarchy); + + res.status(200).json({ + statusCode: 200, + status: true, + message: 'Category tree fetched successfully', + data: hierarchy, + }); + } catch (error) { + next(error); + } +}; + +// Get all available colors for a category +// const Product = require('../../models/mongodb/Product'); +// controllers/products/productController.js +exports.getCategoryColors = async (req, res) => { + try { + const { categorySlug } = req.params; + + // 1๏ธโƒฃ Find category ID from Prisma (PostgreSQL) + const category = await prisma.category.findFirst({ + where: { slug: categorySlug }, + select: { id: true, name: true }, + }); + + if (!category) { + return res.json({ + status: false, + message: 'Category not found', + data: [], + }); + } + + // 2๏ธโƒฃ Fetch products from MongoDB using category ID + const products = await Product.find( + { category: category.id, status: 'active' }, + { variants: 1, images: 1 } + ); + + if (!products.length) { + return res.json({ + status: true, + message: 'No products found in this category', + data: [], + }); + } + + // 3๏ธโƒฃ Extract unique colors + const colorMap = new Map(); + + products.forEach((product) => { + if (product.variants?.length) { + product.variants.forEach((variant) => { + if (!variant.color) return; // skip if no color + + const key = variant.color.toLowerCase(); + + if (!colorMap.has(key)) { + colorMap.set(key, { + name: variant.color, + slug: key, + image: variant.images?.[0] || product.images?.primary || null, + bg: getColorBg(key), // Optional: your helper to generate color background + }); + } + }); + } + }); + + // 4๏ธโƒฃ Prepare response + const colors = Array.from(colorMap.values()); + + const message = + colors.length === 0 + ? 'No color variants available for this category' + : colors.length === 1 + ? `Only 1 color available: ${colors[0].name}` + : 'Category colors fetched successfully'; + + res.json({ + status: true, + message, + data: colors, + }); + } catch (error) { + console.error('Category Colors Error:', error); + res.status(500).json({ + status: false, + message: 'Failed to fetch category colors', + data: [], + }); + } +}; + + + +// Helper function to assign background color (optional) +function getColorBg(colorKey) { + const defaultBg = '#F5F5F5'; + const colorMap = { + white: '#FFFFFF', + black: '#000000', + red: '#FF0000', + blue: '#007BFF', + green: '#28A745', + yellow: '#FFC107', + // Add more colors if needed + }; + + return colorMap[colorKey] || defaultBg; +} + + + +/* helpers */ +const capitalize = str => str.charAt(0).toUpperCase() + str.slice(1); + +function getColorBg(color) { + const map = { + white: '#F5F5F5', + black: '#E5E5E5', + red: '#FDE6E6', + blue: '#E6F0FD', + green: '#E6FDEB', + pink: '#FDE6F1', + maroon: '#FDE6EB', + beige: '#FDEDE6', + grey: '#EFEFEF', + gray: '#EFEFEF', + brown: '#EEE6D8', + yellow: '#FFF7CC', + orange: '#FFE5CC', + purple: '#F1E6FD', + }; + return map[color] || '#F3F4F6'; +} + +/** + * @desc Get New Arrivals + * @route GET /api/products/new-arrivals + * @access Public + */ +exports.getNewArrivals = async (req, res) => { + try { + const limit = parseInt(req.query.limit) || 10; + + // Fetch latest active products + const products = await Product.find({ status: 'active' }) + .sort({ createdAt: -1 }) // newest first + .limit(limit); + + res.status(200).json({ success: true, data: products }); + } catch (error) { + console.error('Error fetching new arrivals:', error); + res.status(500).json({ success: false, message: 'Server Error' }); + } +}; + +// controllers/products/productController.js + +// @desc Get Most Loved / Discounted Products +// @route GET /api/products/most-loved +// @access Public +// Get Most Loved Products (based on purchaseCount or discounted items) +// @desc Get Most Loved / Discounted Products +// @route GET /api/products/most-loved +// @access Public +exports.getMostLovedProducts = async (req, res) => { + try { + const limit = parseInt(req.query.limit) || 8; + + // Fetch products with discount or highest purchase count + let products = await Product.find({ + status: 'active', + $or: [ + { compareAtPrice: { $gt: 0 } }, // discounted + { purchaseCount: { $gt: 0 } }, // most loved + ], + }) + .sort({ purchaseCount: -1, createdAt: -1 }) + .limit(limit); + + // If no products match, return latest active products as fallback + if (!products.length) { + products = await Product.find({ status: 'active' }) + .sort({ createdAt: -1 }) + .limit(limit); + + return res.status(200).json({ + status: true, + message: 'Fallback products fetched successfully', + data: { products }, + }); + } + + // Return the main products + return res.status(200).json({ + status: true, + message: 'Products fetched successfully', + data: { products }, + }); + } catch (error) { + console.error('Error fetching most loved products:', error); + return res.status(500).json({ status: false, message: 'Server Error' }); + } +}; diff --git a/src/controllers/products/recommendation.js b/src/controllers/products/recommendation.js new file mode 100644 index 0000000..9040ade --- /dev/null +++ b/src/controllers/products/recommendation.js @@ -0,0 +1,345 @@ +// controllers/products/recommendationController.js + +const Product = require('../../models/mongodb/Product'); +const { prisma } = require('../../config/database'); + +/** + * @desc Get recommendations for a product + * @route GET /api/products/:slug/recommendations + * @access Public + */ +const getProductRecommendations = async (req, res, next) => { + try { + const { slug } = req.params; + const { limit = 12 } = req.query; + + // Get the current product + const currentProduct = await Product.findOne({ slug, status: 'active' }); + + if (!currentProduct) { + return res.status(404).json({ + success: false, + message: 'Product not found', + }); + } + + // Build recommendation query + const recommendations = await getRecommendedProducts( + currentProduct, + parseInt(limit) + ); + + res.status(200).json({ + success: true, + count: recommendations.length, + data: recommendations, + }); + } catch (error) { + console.error('Get recommendations error:', error); + next(error); + } +}; + +/** + * @desc Get "Customers also bought" products + * @route GET /api/products/:slug/also-bought + * @access Public + */ +const getAlsoBoughtProducts = async (req, res, next) => { + try { + const { slug } = req.params; + const { limit = 8 } = req.query; + + const currentProduct = await Product.findOne({ slug, status: 'active' }); + + if (!currentProduct) { + return res.status(404).json({ + success: false, + message: 'Product not found', + }); + } + + // Get products frequently bought together + const alsoBought = await getFrequentlyBoughtTogether( + currentProduct._id.toString(), + parseInt(limit) + ); + + res.status(200).json({ + success: true, + count: alsoBought.length, + data: alsoBought, + }); + } catch (error) { + console.error('Get also bought error:', error); + next(error); + } +}; + +/** + * @desc Get similar products + * @route GET /api/products/:slug/similar + * @access Public + */ +const getSimilarProducts = async (req, res, next) => { + try { + const { slug } = req.params; + const { limit = 10 } = req.query; + + const currentProduct = await Product.findOne({ slug, status: 'active' }); + + if (!currentProduct) { + return res.status(404).json({ + success: false, + message: 'Product not found', + }); + } + + // Get similar products based on attributes + const similar = await getSimilarProductsByAttributes( + currentProduct, + parseInt(limit) + ); + + res.status(200).json({ + success: true, + count: similar.length, + data: similar, + }); + } catch (error) { + console.error('Get similar products error:', error); + next(error); + } +}; + +/** + * @desc Get personalized recommendations for user + * @route GET /api/products/recommendations/personalized + * @access Private + */ +const getPersonalizedRecommendations = async (req, res, next) => { + try { + const userId = req.user?.id; + const { limit = 20 } = req.query; + + if (!userId) { + // Return popular products for non-authenticated users + const popularProducts = await Product.find({ status: 'active' }) + .sort({ purchaseCount: -1, viewCount: -1 }) + .limit(parseInt(limit)); + + return res.status(200).json({ + success: true, + count: popularProducts.length, + data: popularProducts, + }); + } + + // Get user's purchase history + const userOrders = await prisma.order.findMany({ + where: { + userId, + status: 'DELIVERED', + }, + include: { + items: true, + }, + take: 10, + orderBy: { createdAt: 'desc' }, + }); + + // Get user's wishlist + const wishlist = await prisma.wishlistItem.findMany({ + where: { userId }, + take: 20, + }); + + // Extract product IDs + const purchasedProductIds = userOrders.flatMap((order) => + order.items.map((item) => item.productId) + ); + const wishlistProductIds = wishlist.map((item) => item.productId); + + // Get categories and tags from purchased products + const purchasedProducts = await Product.find({ + _id: { $in: purchasedProductIds }, + }); + + const categories = [...new Set(purchasedProducts.map((p) => p.category))]; + const tags = [ + ...new Set(purchasedProducts.flatMap((p) => p.tags || [])), + ]; + + // Build personalized recommendations + const recommendations = await Product.find({ + status: 'active', + _id: { + $nin: [...purchasedProductIds, ...wishlistProductIds], + }, + $or: [ + { category: { $in: categories } }, + { tags: { $in: tags } }, + { isFeatured: true }, + ], + }) + .sort({ purchaseCount: -1, viewCount: -1 }) + .limit(parseInt(limit)); + + res.status(200).json({ + success: true, + count: recommendations.length, + data: recommendations, + }); + } catch (error) { + console.error('Get personalized recommendations error:', error); + next(error); + } +}; + +/** + * Helper Functions + */ + +// Get recommended products based on multiple factors +async function getRecommendedProducts(currentProduct, limit) { + const priceRange = { + min: currentProduct.basePrice * 0.7, + max: currentProduct.basePrice * 1.3, + }; + + // Score-based recommendation + const recommendations = await Product.aggregate([ + { + $match: { + _id: { $ne: currentProduct._id }, + status: 'active', + }, + }, + { + $addFields: { + score: { + $add: [ + { $cond: [{ $eq: ['$category', currentProduct.category] }, 50, 0] }, + { + $cond: [ + { + $and: [ + { $gte: ['$basePrice', priceRange.min] }, + { $lte: ['$basePrice', priceRange.max] }, + ], + }, + 30, + 0, + ], + }, + { + $multiply: [ + { + $size: { + $ifNull: [ + { + $setIntersection: [ + { $ifNull: ['$tags', []] }, + currentProduct.tags || [], + ], + }, + [], + ], + }, + }, + 5, + ], + }, + { $cond: ['$isFeatured', 20, 0] }, + { $divide: [{ $ifNull: ['$viewCount', 0] }, 100] }, + { $divide: [{ $multiply: [{ $ifNull: ['$purchaseCount', 0] }, 10] }, 10] }, + ], + }, + }, + }, + { $sort: { score: -1 } }, + { $limit: limit }, + ]); + + return recommendations; +} + +// Get frequently bought together products +async function getFrequentlyBoughtTogether(productId, limit) { + try { + const ordersWithProduct = await prisma.orderItem.findMany({ + where: { productId }, + select: { orderId: true }, + distinct: ['orderId'], + }); + + if (ordersWithProduct.length === 0) { + const product = await Product.findById(productId); + return await Product.find({ + _id: { $ne: productId }, + category: product.category, + status: 'active', + }) + .sort({ purchaseCount: -1 }) + .limit(limit); + } + + const orderIds = ordersWithProduct.map((item) => item.orderId); + + const otherProducts = await prisma.orderItem.findMany({ + where: { + orderId: { in: orderIds }, + productId: { not: productId }, + }, + select: { productId: true }, + }); + + const productFrequency = {}; + otherProducts.forEach((item) => { + productFrequency[item.productId] = + (productFrequency[item.productId] || 0) + 1; + }); + + const sortedProductIds = Object.entries(productFrequency) + .sort(([, a], [, b]) => b - a) + .slice(0, limit) + .map(([id]) => id); + + const products = await Product.find({ + _id: { $in: sortedProductIds }, + status: 'active', + }); + + return products; + } catch (error) { + console.error('Get frequently bought together error:', error); + return []; + } +} + +// Get similar products by attributes +async function getSimilarProductsByAttributes(currentProduct, limit) { + const similar = await Product.find({ + _id: { $ne: currentProduct._id }, + status: 'active', + $or: [ + { category: currentProduct.category }, + { tags: { $in: currentProduct.tags || [] } }, + { brand: currentProduct.brand }, + ], + }) + .sort({ + purchaseCount: -1, + viewCount: -1, + }) + .limit(limit); + + return similar; +} + +module.exports = { + getProductRecommendations, + getAlsoBoughtProducts, + getSimilarProducts, + getPersonalizedRecommendations, +}; \ No newline at end of file diff --git a/src/controllers/users/addressController.js b/src/controllers/users/addressController.js new file mode 100644 index 0000000..9c72ea0 --- /dev/null +++ b/src/controllers/users/addressController.js @@ -0,0 +1,128 @@ +const { prisma } = require('../../config/database'); + +// Get addresses +exports.getAddresses = async (req, res, next) => { + try { + const addresses = await prisma.address.findMany({ + where: { userId: req.user.id }, + orderBy: { createdAt: 'desc' }, + }); + // res.json({ success: true, data: { addresses } }); + return res.json({ + statusCode: 200, + status: true, + message: 'Addresses fetched successfully', + data: { addresses }, + }); + } catch (error) { + next(error); + } +}; + +// Add address +exports.addAddress = async (req, res, next) => { + try { + const data = { ...req.body, userId: req.user.id }; + + if (data.isDefault) { + await prisma.address.updateMany({ + where: { userId: req.user.id }, + data: { isDefault: false }, + }); + } + + const address = await prisma.address.create({ data }); + // res.status(201).json({ success: true, message: 'Address added successfully', data: { address } }); + return res.status(201).json({ + statusCode: 201, + status: true, + message: 'Address added successfully', + data: { address }, + }); + } catch (error) { + next(error); + } +}; + +// Update address +exports.updateAddress = async (req, res, next) => { + try { + const { id } = req.params; + const existingAddress = await prisma.address.findFirst({ + where: { id, userId: req.user.id }, + }); + + // if (!existingAddress) { + // return res + // .status(404) + // .json({ success: false, message: 'Address not found' }); + // } + + if (!existingAddress) { + return res.status(404).json({ + statusCode: 404, + status: false, + message: 'Address not found', + }); + } + + if (req.body.isDefault) { + await prisma.address.updateMany({ + where: { userId: req.user.id }, + data: { isDefault: false }, + }); + } + + const updatedAddress = await prisma.address.update({ + where: { id }, + data: req.body, + }); + // res.json({ + // success: true, + // message: 'Address updated successfully', + // data: { address: updatedAddress }, + // }); + return res.status(200).json({ + statusCode: 200, + status: true, + message: 'Address updated successfully', + data: { address: updatedAddress }, + }); + } catch (error) { + next(error); + } +}; + +// Delete address +exports.deleteAddress = async (req, res, next) => { + try { + const { id } = req.params; + const existing = await prisma.address.findFirst({ + where: { id, userId: req.user.id }, + }); + + // if (!existing) { + // return res + // .status(404) + // .json({ success: false, message: 'Address not found' }); + // } + + if (!existing) { + return res.status(404).json({ + statusCode: 404, + status: false, + message: 'Address not found', + }); + } + + await prisma.address.delete({ where: { id } }); + // res.json({ success: true, message: 'Address deleted successfully' }); + return res.status(200).json({ + statusCode: 200, + status: true, + message: 'Address deleted successfully', + }); + } catch (error) { + next(error); + } +}; diff --git a/src/controllers/users/adminUserController.js b/src/controllers/users/adminUserController.js new file mode 100644 index 0000000..a9cdc2e --- /dev/null +++ b/src/controllers/users/adminUserController.js @@ -0,0 +1,70 @@ +const { prisma } = require('../../config/database'); + +exports.getAllUsers = async (req, res, next) => { + try { + const { page = 1, limit = 20, role, search } = req.query; + const skip = (page - 1) * limit; + const where = {}; + + if (role) where.role = role; + if (search) { + where.OR = [ + { email: { contains: search, mode: 'insensitive' } }, + { firstName: { contains: search, mode: 'insensitive' } }, + { lastName: { contains: search, mode: 'insensitive' } }, + { username: { contains: search, mode: 'insensitive' } }, + ]; + } + + const [users, total] = await Promise.all([ + prisma.user.findMany({ + where, + select: { + id: true, + email: true, + username: true, + firstName: true, + lastName: true, + role: true, + isVerified: true, + isActive: true, + createdAt: true, + lastLoginAt: true, + }, + orderBy: { createdAt: 'desc' }, + skip: parseInt(skip), + take: parseInt(limit), + }), + prisma.user.count({ where }), + ]); + + // res.json({ + // success: true, + // data: { + // users, + // pagination: { + // page: parseInt(page), + // limit: parseInt(limit), + // total, + // pages: Math.ceil(total / limit), + // }, + // }, + // }); + return res.status(200).json({ + statusCode: 200, + status: true, + message: "Users fetched successfully", + data: { + users, + pagination: { + page: parseInt(page), + limit: parseInt(limit), + total, + pages: Math.ceil(total / limit), + }, + }, + }); + } catch (error) { + next(error); + } +}; diff --git a/src/controllers/users/cartController.js b/src/controllers/users/cartController.js new file mode 100644 index 0000000..cd71bca --- /dev/null +++ b/src/controllers/users/cartController.js @@ -0,0 +1,150 @@ +const mongoose = require("mongoose"); +// const Product = require("../models/Product"); +const Product = require('../../models/mongodb/Product'); +const { PrismaClient } = require("@prisma/client"); +const prisma = new PrismaClient(); + + +// Add to Cart +exports.addToCart = async (req, res, next) => { + try { + const { productId, quantity = 1 } = req.body; + + if (!productId) { + return res.status(400).json({ + status: false, + message: "Product ID is required", + }); + } + + // Validate ObjectId + if (!mongoose.Types.ObjectId.isValid(productId)) { + return res.status(400).json({ + status: false, + message: "Invalid product ID", + }); + } + + // Check if product exists in MongoDB + const productExists = await Product.findById(productId); + if (!productExists) { + return res.status(404).json({ + status: false, + message: "Product not found", + }); + } + + // Check if item already exists in cart + const existing = await prisma.cartItem.findUnique({ + where: { userId_productId: { userId: req.user.id, productId } }, + }); + + if (existing) { + // Update quantity + const updated = await prisma.cartItem.update({ + where: { userId_productId: { userId: req.user.id, productId } }, + data: { quantity: existing.quantity + quantity }, + }); + + return res.status(200).json({ + status: true, + message: "Cart quantity updated", + data: updated, + }); + } + + // Create new cart item + const item = await prisma.cartItem.create({ + data: { + userId: req.user.id, + productId, + quantity, + }, + }); + + return res.status(201).json({ + status: true, + message: "Item added to cart", + data: item, + }); + } catch (error) { + next(error); + } +}; + + +//Get User Cart +exports.getCart = async (req, res, next) => { + try { + const cart = await prisma.cartItem.findMany({ + where: { userId: req.user.id }, + orderBy: { createdAt: "desc" }, + }); + + // Fetch product details from MongoDB + const productIds = cart.map((item) => item.productId); + const products = await Product.find({ _id: { $in: productIds } }); + + // Merge product details into cart + const cartWithProducts = cart.map((item) => ({ + ...item, + product: products.find((p) => p._id.toString() === item.productId), + })); + + return res.status(200).json({ + status: true, + message: "Cart fetched successfully", + data: cartWithProducts, + }); + } catch (error) { + next(error); + } +}; + + +//Update Quantity +exports.updateQuantity = async (req, res, next) => { + try { + const { productId } = req.params; + const { quantity } = req.body; + + if (!quantity || quantity < 1) { + return res.status(400).json({ + status: false, + message: "Quantity must be at least 1", + }); + } + + const updated = await prisma.cartItem.update({ + where: { userId_productId: { userId: req.user.id, productId } }, + data: { quantity }, + }); + + return res.status(200).json({ + status: true, + message: "Cart quantity updated", + data: updated, + }); + } catch (error) { + next(error); + } +}; + + +//Remove From Cart +exports.removeFromCart = async (req, res, next) => { + try { + const { productId } = req.params; + + await prisma.cartItem.delete({ + where: { userId_productId: { userId: req.user.id, productId } }, + }); + + return res.status(200).json({ + status: true, + message: "Item removed from cart", + }); + } catch (error) { + next(error); + } +}; diff --git a/src/controllers/users/orderController.js b/src/controllers/users/orderController.js new file mode 100644 index 0000000..7423ec8 --- /dev/null +++ b/src/controllers/users/orderController.js @@ -0,0 +1,51 @@ +const { prisma } = require('../../config/database'); + +exports.getOrders = async (req, res, next) => { + try { + const { page = 1, limit = 10, status } = req.query; + const skip = (page - 1) * limit; + const where = { userId: req.user.id }; + if (status) where.status = status; + + const [orders, total] = await Promise.all([ + prisma.order.findMany({ + where, + include: { items: true, address: true }, + orderBy: { createdAt: 'desc' }, + skip: parseInt(skip), + take: parseInt(limit), + }), + prisma.order.count({ where }), + ]); + + // res.json({ + // success: true, + // data: { + // orders, + // pagination: { + // page: parseInt(page), + // limit: parseInt(limit), + // total, + // pages: Math.ceil(total / limit), + // }, + // }, + // }); + + return res.status(200).json({ + statusCode: 200, + status: true, + message: "Orders fetched successfully", + data: { + orders, + pagination: { + page: parseInt(page), + limit: parseInt(limit), + total, + pages: Math.ceil(total / limit), + }, + }, + }); + } catch (error) { + next(error); + } +}; diff --git a/src/controllers/users/profileController.js b/src/controllers/users/profileController.js new file mode 100644 index 0000000..cd5450f --- /dev/null +++ b/src/controllers/users/profileController.js @@ -0,0 +1,113 @@ +const { prisma } = require('../../config/database'); +const uploadToS3 = require('../../utils/uploadToS3'); + + +// Get user profile +exports.getProfile = async (req, res, next) => { + try { + const user = await prisma.user.findUnique({ + where: { id: req.user.id }, + select: { + id: true, + email: true, + username: true, + firstName: true, + lastName: true, + phone: true, + avatar: true, + role: true, + isVerified: true, + createdAt: true, + lastLoginAt: true, + }, + }); + + // res.json({ success: true, data: { user } }); + return res.status(200).json({ + statusCode: 200, + status: true, + message: 'Profile fetched successfully', + data: { user }, + }); + } catch (error) { + next(error); + } +}; + +// Update user profile +exports.updateProfile = async (req, res, next) => { + try { + const { firstName, lastName, username, phone } = req.body; + + if (username) { + const existingUser = await prisma.user.findFirst({ + where: { username, NOT: { id: req.user.id } }, + }); + // if (existingUser) { + // return res.status(400).json({ success: false, message: 'Username already taken' }); + // } + if (existingUser) { + return res.status(400).json({ + statusCode: 400, + status: false, + message: 'Username already taken', + }); + } + } + + const updatedUser = await prisma.user.update({ + where: { id: req.user.id }, + data: { firstName, lastName, username, phone }, + select: { + id: true, + email: true, + username: true, + firstName: true, + lastName: true, + phone: true, + avatar: true, + role: true, + isVerified: true, + }, + }); + + // res.json({ success: true, message: 'Profile updated successfully', data: { user: updatedUser } }); + return res.status(200).json({ + statusCode: 200, + status: true, + message: 'Profile updated successfully', + data: { user: updatedUser }, + }); + } catch (error) { + next(error); + } +}; + +// Upload avatar (profile picture) +exports.uploadAvatar = async (req, res, next) => { + try { + if (!req.file) { + return res.status(400).json({ + success: false, + message: "No file uploaded", + }); + } + + const avatarUrl = req.file.location; // multer-s3 gives S3 URL + + // Update avatar in Prisma + await prisma.user.update({ + where: { id: req.user.id }, + data: { avatar: avatarUrl }, + }); + + res.status(200).json({ + success: true, + message: "Profile picture uploaded successfully", + avatarUrl, // only return the profile picture link + }); + } catch (error) { + next(error); + } +}; + diff --git a/src/controllers/users/wishlistController.js b/src/controllers/users/wishlistController.js new file mode 100644 index 0000000..df42750 --- /dev/null +++ b/src/controllers/users/wishlistController.js @@ -0,0 +1,245 @@ +const { prisma } = require('../../config/database'); +// import Product from '../../models/mongodb/Product'; +const Product = require('../../models/mongodb/Product'); + + +// exports.getWishlist = async (req, res, next) => { +// try { +// const wishlist = await prisma.wishlistItem.findMany({ +// where: { userId: req.user.id }, +// orderBy: { createdAt: 'desc' }, +// }); + +// // Fetch product details from MongoDB +// const detailedWishlist = await Promise.all( +// wishlist.map(async item => { +// const product = await Product.findById(item.productId).select( +// 'name basePrice variants images' +// ); + +// return { +// ...item, +// product: product || null, +// }; +// }) +// ); + +// // res.json({ success: true, data: { wishlist } }); +// return res.status(200).json({ +// // statusCode: 200, +// status: true, +// message: 'Wishlist fetched successfully', +// // data: { wishlist }, +// data: { wishlist: detailedWishlist }, +// }); +// } catch (error) { +// next(error); +// } +// }; + +// exports.addToWishlist = async (req, res, next) => { +// try { +// const { productId } = req.body; +// // if (!productId) return res.status(400).json({ success: false, message: 'Product ID is required' }); + +// if (!productId) { +// return res.status(400).json({ +// statusCode: 400, +// status: false, +// message: 'Product ID is required', +// }); +// } + +// const existing = await prisma.wishlistItem.findUnique({ +// where: { userId_productId: { userId: req.user.id, productId } }, +// }); +// // if (existing) return res.status(400).json({ success: false, message: 'Item already in wishlist' }); +// if (existing) { +// return res.status(400).json({ +// statusCode: 400, +// status: false, +// message: 'Item already in wishlist', +// }); +// } + +// const item = await prisma.wishlistItem.create({ +// data: { userId: req.user.id, productId }, +// }); +// // res.status(201).json({ success: true, message: 'Item added to wishlist', data: { item } }); +// return res.status(201).json({ +// statusCode: 201, +// status: true, +// message: 'Item added to wishlist', +// data: { item }, +// }); +// } catch (error) { +// next(error); +// } +// }; + +const mongoose = require("mongoose"); + +exports.getWishlist = async (req, res, next) => { + try { + const wishlist = await prisma.wishlistItem.findMany({ + where: { userId: req.user.id }, + orderBy: { createdAt: "desc" }, + }); + + const detailedWishlist = await Promise.all( + wishlist.map(async (item) => { + let product = null; + + // Only try MongoDB lookup if valid ObjectId + if (mongoose.Types.ObjectId.isValid(item.productId)) { + product = await Product.findById(item.productId).select( + "name basePrice variants images" + ); + } + + return { + ...item, + product: product || null, + }; + }) + ); + + return res.status(200).json({ + status: true, + message: "Wishlist fetched successfully", + data: { wishlist: detailedWishlist }, + }); + } catch (error) { + next(error); + } +}; + + +// exports.addToWishlist = async (req, res, next) => { +// try { +// const { productId } = req.body; + +// if (!productId) { +// return res.status(400).json({ +// statusCode: 400, +// status: false, +// message: 'Product ID is required', +// }); +// } + +// const existing = await prisma.wishlistItem.findUnique({ +// where: { userId_productId: { userId: req.user.id, productId } }, +// }); + +// if (existing) { +// return res.status(400).json({ +// statusCode: 400, +// status: false, +// message: 'Item already in wishlist', +// }); +// } + +// const item = await prisma.wishlistItem.create({ +// data: { userId: req.user.id, productId }, +// }); + +// return res.status(201).json({ +// statusCode: 201, +// status: true, +// message: 'Item added to wishlist', +// data: { item }, +// }); +// } catch (error) { +// next(error); +// } +// }; + + +// const mongoose = require("mongoose"); + +exports.addToWishlist = async (req, res, next) => { + try { + const { productId } = req.body; + + if (!productId) { + return res.status(400).json({ + statusCode: 400, + status: false, + message: "Product ID is required", + }); + } + + // 1๏ธโƒฃ Validate ObjectId + if (!mongoose.Types.ObjectId.isValid(productId)) { + return res.status(400).json({ + status: false, + message: "Invalid product ID (must be MongoDB ObjectId)", + }); + } + + // 2๏ธโƒฃ Ensure product exists in MongoDB + const productExists = await Product.findById(productId); + if (!productExists) { + return res.status(404).json({ + status: false, + message: "Product not found in database", + }); + } + + // 3๏ธโƒฃ Check duplicate + const existing = await prisma.wishlistItem.findUnique({ + where: { userId_productId: { userId: req.user.id, productId } }, + }); + + if (existing) { + return res.status(400).json({ + status: false, + message: "Item already in wishlist", + }); + } + + // 4๏ธโƒฃ Save VALID productId in Prisma + const item = await prisma.wishlistItem.create({ + data: { userId: req.user.id, productId }, + }); + + return res.status(201).json({ + status: true, + message: "Item added to wishlist", + data: { item }, + }); + } catch (error) { + next(error); + } +}; + + +exports.removeFromWishlist = async (req, res, next) => { + try { + const { productId } = req.params; + const existing = await prisma.wishlistItem.findUnique({ + where: { userId_productId: { userId: req.user.id, productId } }, + }); + // if (!existing) return res.status(404).json({ success: false, message: 'Item not found' }); + + if (!existing) { + return res.status(404).json({ + statusCode: 404, + status: false, + message: 'Item not found in wishlist', + }); + } + + await prisma.wishlistItem.delete({ + where: { userId_productId: { userId: req.user.id, productId } }, + }); + // res.json({ success: true, message: 'Item removed from wishlist' }); + return res.status(200).json({ + statusCode: 200, + status: true, + message: 'Item removed from wishlist', + }); + } catch (error) { + next(error); + } +}; diff --git a/src/controllers/wardrobe/wardrobeItemController.js b/src/controllers/wardrobe/wardrobeItemController.js new file mode 100644 index 0000000..1ad2819 --- /dev/null +++ b/src/controllers/wardrobe/wardrobeItemController.js @@ -0,0 +1,200 @@ +const Wardrobe = require('../../models/mongodb/Wardrobe'); + +// @desc Add item to wardrobe +// exports.addItem = async (req, res, next) => { +// try { +// const itemData = req.body; + +// let wardrobe = await Wardrobe.findByUserId(req.user.id); + +// if (!wardrobe) { +// wardrobe = new Wardrobe({ userId: req.user.id, name: 'My Wardrobe' }); +// } + +// await wardrobe.addItem(itemData); + +// // res.status(201).json({ +// // success: true, +// // message: 'Item added successfully', +// // data: { wardrobe }, +// // }); +// return res.status(201).json({ +// statusCode: 201, +// status: true, +// message: 'Item added successfully', +// data: { wardrobe }, +// }); +// } catch (error) { +// next(error); +// } +// }; + + + +exports.addItem = async (req, res, next) => { + try { + const itemData = { ...req.body }; + + // โœ… FIX: map image โ†’ images array + if (itemData.image) { + itemData.images = [ + { + url: itemData.image, + isPrimary: true, + } + ]; + delete itemData.image; // prevent schema pollution + } + + let wardrobe = await Wardrobe.findByUserId(req.user.id); + + if (!wardrobe) { + wardrobe = new Wardrobe({ + userId: req.user.id, + name: 'My Wardrobe', + }); + } + + await wardrobe.addItem(itemData); + + return res.status(201).json({ + statusCode: 201, + status: true, + message: 'Item added successfully', + data: { wardrobe }, + }); + } catch (error) { + next(error); + } +}; + + + + + +// @desc Update wardrobe item +exports.updateItem = async (req, res, next) => { + try { + const { itemId } = req.params; + const updateData = req.body; + const wardrobe = await Wardrobe.findByUserId(req.user.id); + + // if (!wardrobe) { + // return res.status(404).json({ success: false, message: 'Wardrobe not found' }); + // } + + if (!wardrobe) { + return res.status(404).json({ + statusCode: 404, + status: false, + message: 'Wardrobe not found', + }); + } + + await wardrobe.updateItem(itemId, updateData); + + // res.json({ success: true, message: 'Item updated successfully', data: { wardrobe } }); + return res.json({ + statusCode: 200, + status: true, + message: 'Item updated successfully', + data: { wardrobe }, + }); + } catch (error) { + next(error); + } +}; + +// @desc Remove item from wardrobe +exports.removeItem = async (req, res, next) => { + try { + const { itemId } = req.params; + const wardrobe = await Wardrobe.findByUserId(req.user.id); + + // if (!wardrobe) { + // return res.status(404).json({ success: false, message: 'Wardrobe not found' }); + // } + + if (!wardrobe) { + return res.status(404).json({ + statusCode: 404, + status: false, + message: 'Wardrobe not found', + }); + } + + await wardrobe.removeItem(itemId); + + // res.json({ success: true, message: 'Item removed successfully', data: { wardrobe } }); + return res.json({ + statusCode: 200, + status: true, + message: 'Item removed successfully', + data: { wardrobe }, + }); + } catch (error) { + next(error); + } +}; + +// @desc Get items by category +// exports.getItemsByCategory = async (req, res, next) => { +// try { +// const { category } = req.params; +// const wardrobe = await Wardrobe.findByUserId(req.user.id); + +// // if (!wardrobe) return res.status(404).json({ success: false, message: 'Wardrobe not found' }); + +// if (!wardrobe) { +// return res.status(404).json({ +// statusCode: 404, +// status: false, +// message: 'Wardrobe not found', +// }); +// } + +// const items = wardrobe.getItemsByCategory(category); +// // res.json({ success: true, data: { items } }); +// return res.json({ +// statusCode: 200, +// status: true, +// message: 'Items fetched successfully', +// data: { items }, +// }); +// } catch (error) { +// next(error); +// } +// }; + + +// controllers/wardrobeController.js +exports.getItemsByCategory = async (req, res, next) => { + try { + const { category } = req.params; + const wardrobe = await Wardrobe.findByUserId(req.user.id); + + if (!wardrobe) { + return res.status(404).json({ + statusCode: 404, + status: false, + message: "Wardrobe not found", + }); + } + + const items = wardrobe.getItemsByCategory(category) || []; + + return res.json({ + statusCode: 200, + status: true, + message: "Items fetched successfully", + data: { items }, + }); + } catch (error) { + console.error("Error fetching items by category:", error); + return res.status(500).json({ + statusCode: 500, + status: false, + message: "Internal server error", + }); + } +}; diff --git a/src/controllers/wardrobe/wardrobeMainController.js b/src/controllers/wardrobe/wardrobeMainController.js new file mode 100644 index 0000000..c671072 --- /dev/null +++ b/src/controllers/wardrobe/wardrobeMainController.js @@ -0,0 +1,66 @@ +const Wardrobe = require('../../models/mongodb/Wardrobe'); + +// @desc Get user's wardrobe +exports.getWardrobe = async (req, res, next) => { + try { + let wardrobe = await Wardrobe.findByUserId(req.user.id); + + if (!wardrobe) { + wardrobe = new Wardrobe({ + userId: req.user.id, + name: 'My Wardrobe', + }); + await wardrobe.save(); + } + + // res.json({ success: true, data: { wardrobe } }); + return res.status(200).json({ + statusCode: 200, + status: true, + message: "Wardrobe fetched successfully", + data: { wardrobe }, + }); + } catch (error) { + next(error); + } +}; + +// @desc Update wardrobe details +exports.updateWardrobe = async (req, res, next) => { + try { + const { name, description, isPublic, shareSettings } = req.body; + + let wardrobe = await Wardrobe.findByUserId(req.user.id); + + if (!wardrobe) { + wardrobe = new Wardrobe({ + userId: req.user.id, + name: name || 'My Wardrobe', + description, + isPublic: isPublic || false, + shareSettings: shareSettings || { allowViewing: false, allowRecommendations: false }, + }); + } else { + wardrobe.name = name || wardrobe.name; + wardrobe.description = description; + wardrobe.isPublic = isPublic ?? wardrobe.isPublic; + wardrobe.shareSettings = shareSettings || wardrobe.shareSettings; + } + + await wardrobe.save(); + + // res.json({ + // success: true, + // message: 'Wardrobe updated successfully', + // data: { wardrobe }, + // }); + return res.status(200).json({ + statusCode: 200, + status: true, + message: "Wardrobe updated successfully", + data: { wardrobe }, + }); + } catch (error) { + next(error); + } +}; diff --git a/src/controllers/wardrobe/wardrobePublicController.js b/src/controllers/wardrobe/wardrobePublicController.js new file mode 100644 index 0000000..32d461c --- /dev/null +++ b/src/controllers/wardrobe/wardrobePublicController.js @@ -0,0 +1,48 @@ +const Wardrobe = require('../../models/mongodb/Wardrobe'); + +// @desc Get public wardrobes +exports.getPublicWardrobes = async (req, res, next) => { + try { + const { page = 1, limit = 20 } = req.query; + const skip = (page - 1) * limit; + + const wardrobes = await Wardrobe.findPublicWardrobes(parseInt(limit), parseInt(skip)); + + // res.json({ success: true, data: { wardrobes } }); + return res.status(200).json({ + statusCode: 200, + status: true, + message: "Public wardrobes fetched successfully", + data: { wardrobes }, + }); + } catch (error) { + next(error); + } +}; + +// @desc Get public wardrobe by ID +exports.getPublicWardrobeById = async (req, res, next) => { + try { + const wardrobe = await Wardrobe.findOne({ _id: req.params.id, isPublic: true }); + + // if (!wardrobe) return res.status(404).json({ success: false, message: 'Wardrobe not found' }); + + if (!wardrobe) { + return res.status(404).json({ + statusCode: 404, + status: false, + message: "Wardrobe not found", + }); + } + + // res.json({ success: true, data: { wardrobe } }); + return res.status(200).json({ + statusCode: 200, + status: true, + message: "Public wardrobe fetched successfully", + data: { wardrobe }, + }); + } catch (error) { + next(error); + } +}; diff --git a/src/controllers/wardrobe/wardrobeRecommendationController.js b/src/controllers/wardrobe/wardrobeRecommendationController.js new file mode 100644 index 0000000..5d36753 --- /dev/null +++ b/src/controllers/wardrobe/wardrobeRecommendationController.js @@ -0,0 +1,29 @@ +const Wardrobe = require('../../models/mongodb/Wardrobe'); + +// @desc Generate outfit recommendations +exports.getRecommendations = async (req, res, next) => { + try { + const wardrobe = await Wardrobe.findByUserId(req.user.id); + // if (!wardrobe) return res.status(404).json({ success: false, message: 'Wardrobe not found' }); + + if (!wardrobe) { + return res.status(404).json({ + statusCode: 404, + status: false, + message: "Wardrobe not found", + data: null + }); + } + + const recommendations = wardrobe.generateOutfitRecommendations(); + // res.json({ success: true, data: { recommendations } }); + return res.status(200).json({ + statusCode: 200, + status: true, + message: "Recommendations fetched successfully", + data: { recommendations } + }); + } catch (error) { + next(error); + } +}; diff --git a/src/controllers/wardrobe/wardrobeSearchController.js b/src/controllers/wardrobe/wardrobeSearchController.js new file mode 100644 index 0000000..601622f --- /dev/null +++ b/src/controllers/wardrobe/wardrobeSearchController.js @@ -0,0 +1,52 @@ +const Wardrobe = require('../../models/mongodb/Wardrobe'); + +// @desc Search wardrobe items +exports.searchItems = async (req, res, next) => { + try { + const { query, tags, category } = req.query; + const wardrobe = await Wardrobe.findByUserId(req.user.id); + + // if (!wardrobe) return res.status(404).json({ success: false, message: 'Wardrobe not found' }); + + if (!wardrobe) { + return res.status(404).json({ + statusCode: 404, + status: false, + message: "Wardrobe not found", + data: null + }); + } + + let items = wardrobe.items.filter(item => item.isActive); + + if (category) items = items.filter(item => item.category === category); + + if (tags) { + const tagArray = tags.split(','); + items = items.filter(item => + tagArray.some(tag => + item.aiTags.includes(tag) || item.userTags.includes(tag) + ) + ); + } + + if (query) { + const searchTerm = query.toLowerCase(); + items = items.filter(item => + item.name.toLowerCase().includes(searchTerm) || + item.brand?.toLowerCase().includes(searchTerm) || + item.description?.toLowerCase().includes(searchTerm) + ); + } + + // res.json({ success: true, data: { items } }); + return res.status(200).json({ + statusCode: 200, + status: true, + message: "Items fetched successfully", + data: { items } + }); + } catch (error) { + next(error); + } +}; diff --git a/src/controllers/wardrobe/wardrobeStatsController.js b/src/controllers/wardrobe/wardrobeStatsController.js new file mode 100644 index 0000000..8467252 --- /dev/null +++ b/src/controllers/wardrobe/wardrobeStatsController.js @@ -0,0 +1,39 @@ +const Wardrobe = require('../../models/mongodb/Wardrobe'); + +// @desc Get wardrobe statistics +exports.getStats = async (req, res, next) => { + try { + const wardrobe = await Wardrobe.findByUserId(req.user.id); + // if (!wardrobe) return res.status(404).json({ success: false, message: 'Wardrobe not found' }); + + if (!wardrobe) { + return res.status(404).json({ + statusCode: 404, + status: false, + message: "Wardrobe not found", + data: null + }); + } + + const stats = { + totalItems: wardrobe.totalItems, + categoryCounts: wardrobe.categoryCounts, + recentItems: wardrobe.items + .sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt)) + .slice(0, 5), + mostWornItems: wardrobe.items + .sort((a, b) => (b.purchaseCount || 0) - (a.purchaseCount || 0)) + .slice(0, 5), + }; + + // res.json({ success: true, data: { stats } }); + return res.status(200).json({ + statusCode: 200, + status: true, + message: "Wardrobe statistics fetched successfully", + data: { stats } + }); + } catch (error) { + next(error); + } +}; diff --git a/src/middleware/auth.js b/src/middleware/auth.js new file mode 100644 index 0000000..9bcc4c3 --- /dev/null +++ b/src/middleware/auth.js @@ -0,0 +1,153 @@ +const jwt = require('jsonwebtoken'); +const { prisma } = require('../config/database'); + +// Protect routes - require authentication +const protect = async (req, res, next) => { + let token; + + // Check for token in header + if (req.headers.authorization && req.headers.authorization.startsWith('Bearer')) { + try { + // Get token from header + token = req.headers.authorization.split(' ')[1]; + + // Verify token + const decoded = jwt.verify(token, process.env.JWT_SECRET); + + // Get user from database + const user = await prisma.user.findUnique({ + where: { id: decoded.id }, + select: { + id: true, + email: true, + username: true, + firstName: true, + lastName: true, + role: true, + isActive: true, + isVerified: true, + }, + }); + + if (!user) { + return res.status(401).json({ + success: false, + message: 'Not authorized, user not found', + }); + } + + if (!user.isActive) { + return res.status(401).json({ + success: false, + message: 'Not authorized, account is deactivated', + }); + } + + req.user = user; + next(); + } catch (error) { + console.error('Auth middleware error:', error); + return res.status(401).json({ + success: false, + message: 'Not authorized, token failed', + }); + } + } else { + return res.status(401).json({ + success: false, + message: 'Not authorized, no token', + }); + } +}; + +// Grant access to specific roles +const authorize = (...roles) => { + return (req, res, next) => { + if (!req.user) { + return res.status(401).json({ + success: false, + message: 'Not authorized to access this route', + }); + } + + if (!roles.includes(req.user.role)) { + return res.status(403).json({ + success: false, + message: `User role ${req.user.role} is not authorized to access this route`, + }); + } + + next(); + }; +}; + +// Optional auth - doesn't fail if no token +const optionalAuth = async (req, res, next) => { + let token; + + if (req.headers.authorization && req.headers.authorization.startsWith('Bearer')) { + try { + token = req.headers.authorization.split(' ')[1]; + const decoded = jwt.verify(token, process.env.JWT_SECRET); + + const user = await prisma.user.findUnique({ + where: { id: decoded.id }, + select: { + id: true, + email: true, + username: true, + firstName: true, + lastName: true, + role: true, + isActive: true, + isVerified: true, + }, + }); + + if (user && user.isActive) { + req.user = user; + } + } catch (error) { + // Ignore token errors for optional auth + console.log('Optional auth token error:', error.message); + } + } + + next(); +}; + +// Check if user owns resource +const checkOwnership = (resourceUserIdField = 'userId') => { + return (req, res, next) => { + if (!req.user) { + return res.status(401).json({ + success: false, + message: 'Not authorized', + }); + } + + // Admin can access any resource + if (req.user.role === 'ADMIN') { + return next(); + } + + // Check if user owns the resource + const resourceUserId = req.params[resourceUserIdField] || req.body[resourceUserIdField]; + + if (resourceUserId && resourceUserId !== req.user.id) { + return res.status(403).json({ + success: false, + message: 'Not authorized to access this resource', + }); + } + + next(); + }; +}; + +module.exports = { + protect, + authorize, + optionalAuth, + checkOwnership, +}; diff --git a/src/middleware/errorHandler.js b/src/middleware/errorHandler.js new file mode 100644 index 0000000..8b30a87 --- /dev/null +++ b/src/middleware/errorHandler.js @@ -0,0 +1,61 @@ +const errorHandler = (err, req, res, next) => { + let error = { ...err }; + error.message = err.message; + + // Log error + console.error(err); + + // Mongoose bad ObjectId + if (err.name === 'CastError') { + const message = 'Resource not found'; + error = { message, statusCode: 404 }; + } + + // Mongoose duplicate key + if (err.code === 11000) { + const message = 'Duplicate field value entered'; + error = { message, statusCode: 400 }; + } + + // Mongoose validation error + if (err.name === 'ValidationError') { + const message = Object.values(err.errors).map(val => val.message); + error = { message, statusCode: 400 }; + } + + // Prisma errors + if (err.code === 'P2002') { + const message = 'Duplicate field value entered'; + error = { message, statusCode: 400 }; + } + + if (err.code === 'P2025') { + const message = 'Record not found'; + error = { message, statusCode: 404 }; + } + + // JWT errors + if (err.name === 'JsonWebTokenError') { + const message = 'Invalid token'; + error = { message, statusCode: 401 }; + } + + if (err.name === 'TokenExpiredError') { + const message = 'Token expired'; + error = { message, statusCode: 401 }; + } + + res.status(error.statusCode || 500).json({ + success: false, + error: error.message || 'Server Error', + ...(process.env.NODE_ENV === 'development' && { stack: err.stack }), + }); +}; + +const notFound = (req, res, next) => { + const error = new Error(`Not Found - ${req.originalUrl}`); + res.status(404); + next(error); +}; + +module.exports = { errorHandler, notFound }; diff --git a/src/middleware/upload.js b/src/middleware/upload.js new file mode 100644 index 0000000..5f7e9c3 --- /dev/null +++ b/src/middleware/upload.js @@ -0,0 +1,10 @@ +const multer = require("multer"); + +const upload = multer({ + storage: multer.memoryStorage(), + limits: { fileSize: 5 * 1024 * 1024 }, +}); + +module.exports = upload; + + diff --git a/src/middleware/uploadProfile.js b/src/middleware/uploadProfile.js new file mode 100644 index 0000000..1d8c9fa --- /dev/null +++ b/src/middleware/uploadProfile.js @@ -0,0 +1,25 @@ +const multerS3 = require("multer-s3"); +const multer = require("multer"); +const s3 = require("../config/s3"); + +const uploadProfile = multer({ + storage: multerS3({ + s3: s3, + bucket: process.env.AWS_S3_BUCKET, + acl: "public-read", + key: (req, file, cb) => { + const userId = req.user.id; + const ext = file.originalname.split(".").pop(); + cb(null, `profiles/${userId}-${Date.now()}.${ext}`); + }, + }), + limits: { fileSize: 5 * 1024 * 1024 }, + fileFilter: (req, file, cb) => { + if (!file.mimetype.startsWith("image/")) { + return cb(new Error("Only images are allowed!")); + } + cb(null, true); + }, +}); + +module.exports = { uploadProfile }; diff --git a/src/models/mongodb/Product.js b/src/models/mongodb/Product.js new file mode 100644 index 0000000..a432702 --- /dev/null +++ b/src/models/mongodb/Product.js @@ -0,0 +1,310 @@ +const mongoose = require('mongoose'); + +const variantSchema = new mongoose.Schema({ + size: { + type: String, + required: true, + }, + color: { + type: String, + required: true, + }, + sku: { + type: String, + required: true, + }, + price: { + type: Number, + required: true, + min: 0, + }, + compareAtPrice: { + type: Number, + min: 0, + }, + inventory: { + quantity: { + type: Number, + default: 0, + min: 0, + }, + + trackInventory: { + type: Boolean, + default: true, + }, + }, + images: [String], + isActive: { + type: Boolean, + default: true, + }, +}); + +const productSchema = new mongoose.Schema({ + // Basic Information + name: { + type: String, + required: true, + trim: true, + }, + slug: { + type: String, + required: true, + unique: true, + lowercase: true, + }, + description: { + type: String, + required: true, + }, + shortDescription: { + type: String, + maxLength: 500, + }, + + // Categorization + category: { + type: String, + required: true, + }, + subcategory: String, + tags: [String], + brand: String, + + // Pricing & Inventory + basePrice: { + type: Number, + required: true, + min: 0, + }, + compareAtPrice: { + type: Number, + min: 0, + }, + costPrice: { + type: Number, + min: 0, + }, + + // Variants + variants: [variantSchema], + hasVariants: { + type: Boolean, + default: false, + }, + + // Media + images: { + primary: String, + gallery: [String], + videos: [String], + }, + + // SEO + metaTitle: String, + metaDescription: String, + metaKeywords: [String], + + // Status & Visibility + status: { + type: String, + enum: ['draft', 'active', 'inactive', 'archived'], + default: 'draft', + }, + isFeatured: { + type: Boolean, + default: false, + }, + isDigital: { + type: Boolean, + default: false, + }, + + // Physical Attributes + weight: { + value: Number, + unit: { + type: String, + enum: ['g', 'kg', 'lb', 'oz'], + default: 'g', + }, + }, + dimensions: { + length: Number, + width: Number, + height: Number, + unit: { + type: String, + enum: ['cm', 'in'], + default: 'cm', + }, + }, + + // Analytics + viewCount: { + type: Number, + default: 0, + }, + purchaseCount: { + type: Number, + default: 0, + }, + + // AI Generated Tags + aiTags: [String], + aiGeneratedDescription: String, + + // Timestamps + createdAt: { + type: Date, + default: Date.now, + }, + updatedAt: { + type: Date, + default: Date.now, + }, + publishedAt: Date, +}); + +// Indexes for better performance +productSchema.index({ slug: 1 }); +productSchema.index({ category: 1, status: 1 }); +productSchema.index({ brand: 1 }); +productSchema.index({ tags: 1 }); +productSchema.index({ 'variants.sku': 1 }); +productSchema.index({ status: 1, isFeatured: 1 }); +productSchema.index({ createdAt: -1 }); +productSchema.index({ stock: 1 }); + +// Text search index +productSchema.index({ + name: 'text', + description: 'text', + tags: 'text', + brand: 'text', +}); + +// Virtual for average rating (if implementing ratings) +productSchema.virtual('averageRating').get(function () { + // This would be calculated from reviews in PostgreSQL + return 0; +}); + +// Pre-save middleware +productSchema.pre('save', function (next) { + this.updatedAt = new Date(); + + // Auto-generate slug if not provided + if (!this.slug && this.name) { + this.slug = this.name + .toLowerCase() + .replace(/[^a-z0-9]+/g, '-') + .replace(/(^-|-$)/g, ''); + } + + // Set published date when status changes to active + if ( + this.isModified('status') && + this.status === 'active' && + !this.publishedAt + ) { + this.publishedAt = new Date(); + } + + next(); +}); + +// Instance methods +productSchema.methods.incrementViewCount = function () { + this.viewCount += 1; + return this.save(); +}; + +productSchema.methods.incrementPurchaseCount = function (quantity = 1) { + this.purchaseCount += quantity; + return this.save(); +}; + +productSchema.methods.getAvailableVariants = function () { + return this.variants.filter( + variant => + variant.isActive && + (!variant.inventory.trackInventory || variant.inventory.quantity > 0) + ); +}; + +productSchema.methods.getAvailableStock = function () { + if (this.hasVariants && this.variants?.length > 0) { + return this.variants + .filter(v => v.isActive) + .reduce((sum, v) => sum + (v.inventory?.quantity || 0), 0); + } + + return this.stock || 0; +}; + +productSchema.methods.isInStock = function (quantity = 1) { + const availableStock = this.getAvailableStock(); + return availableStock >= quantity; +}; + +productSchema.methods.reduceStock = async function (quantity = 1) { + if (this.hasVariants && this.variants && this.variants.length > 0) { + return { + success: false, + message: 'Use variant-specific stock reduction', + }; + } + + const currentStock = this.stock || 0; + this.stock = Math.max(0, currentStock - quantity); + await this.save(); + + return { + success: true, + previousStock: currentStock, + newStock: this.stock, + reduced: quantity, + }; +}; + +// Static methods +productSchema.statics.findBySlug = function (slug) { + return this.findOne({ slug, status: 'active' }); +}; + +productSchema.statics.findByCategory = function ( + category, + limit = 20, + skip = 0 +) { + return this.find({ category, status: 'active' }) + .limit(limit) + .skip(skip) + .sort({ createdAt: -1 }); +}; + +productSchema.statics.searchProducts = function (query, options = {}) { + const { category, brand, minPrice, maxPrice, limit = 20, skip = 0 } = options; + + const searchQuery = { + $text: { $search: query }, + status: 'active', + }; + + if (category) searchQuery.category = category; + if (brand) searchQuery.brand = brand; + if (minPrice || maxPrice) { + searchQuery.basePrice = {}; + if (minPrice) searchQuery.basePrice.$gte = minPrice; + if (maxPrice) searchQuery.basePrice.$lte = maxPrice; + } + + return this.find(searchQuery) + .limit(limit) + .skip(skip) + .sort({ score: { $meta: 'textScore' } }); +}; + +module.exports = mongoose.model('Product', productSchema); diff --git a/src/models/mongodb/Wardrobe.js b/src/models/mongodb/Wardrobe.js new file mode 100644 index 0000000..0140009 --- /dev/null +++ b/src/models/mongodb/Wardrobe.js @@ -0,0 +1,254 @@ +const mongoose = require('mongoose'); + +const wardrobeItemSchema = new mongoose.Schema({ + // Basic Information + name: { + type: String, + required: true, + }, + description: String, + + // Category & Type + category: { + type: String, + required: true, + enum: ['tops', 'bottoms', 'dresses', 'outerwear', 'shoes', 'accessories', 'other'], + }, + subcategory: String, + brand: String, + color: String, + + // Images + images: [{ + url: { + type: String, + required: true, + }, + isPrimary: { + type: Boolean, + default: false, + }, + uploadedAt: { + type: Date, + default: Date.now, + }, + }], + + // AI Generated Tags + aiTags: [String], + aiColorPalette: [String], + aiStyleTags: [String], + + // User Tags + userTags: [String], + + // Physical Attributes + size: String, + material: String, + condition: { + type: String, + enum: ['new', 'like-new', 'good', 'fair', 'poor'], + default: 'good', + }, + + // Status + isActive: { + type: Boolean, + default: true, + }, + + // Timestamps + createdAt: { + type: Date, + default: Date.now, + }, + updatedAt: { + type: Date, + default: Date.now, + }, +}); + +const wardrobeSchema = new mongoose.Schema({ + userId: { + type: String, + required: true, + ref: 'User', // Reference to PostgreSQL User + }, + + // Wardrobe Information + name: { + type: String, + default: 'My Wardrobe', + }, + description: String, + + // Items + items: [wardrobeItemSchema], + + // Statistics + totalItems: { + type: Number, + default: 0, + }, + categoryCounts: { + tops: { type: Number, default: 0 }, + bottoms: { type: Number, default: 0 }, + dresses: { type: Number, default: 0 }, + outerwear: { type: Number, default: 0 }, + shoes: { type: Number, default: 0 }, + accessories: { type: Number, default: 0 }, + other: { type: Number, default: 0 }, + }, + + // AI Analysis + aiAnalysis: { + lastAnalyzed: Date, + dominantColors: [String], + styleProfile: { + casual: Number, + formal: Number, + trendy: Number, + classic: Number, + bohemian: Number, + minimalist: Number, + }, + recommendations: [String], + }, + + // Privacy Settings + isPublic: { + type: Boolean, + default: false, + }, + shareSettings: { + allowViewing: Boolean, + allowRecommendations: Boolean, + }, + + // Timestamps + createdAt: { + type: Date, + default: Date.now, + }, + updatedAt: { + type: Date, + default: Date.now, + }, +}); + +// Indexes +wardrobeSchema.index({ userId: 1 }); +wardrobeSchema.index({ isPublic: 1 }); +wardrobeSchema.index({ 'items.category': 1 }); +wardrobeSchema.index({ 'items.aiTags': 1 }); + +// Text search index +wardrobeSchema.index({ + name: 'text', + description: 'text', + 'items.name': 'text', + 'items.brand': 'text', + 'items.userTags': 'text', +}); + +// Pre-save middleware +wardrobeSchema.pre('save', function(next) { + this.updatedAt = new Date(); + + // Update statistics + this.totalItems = this.items.length; + + // Reset category counts + this.categoryCounts = { + tops: 0, + bottoms: 0, + dresses: 0, + outerwear: 0, + shoes: 0, + accessories: 0, + other: 0, + }; + + // Count items by category + this.items.forEach(item => { + if (this.categoryCounts[item.category] !== undefined) { + this.categoryCounts[item.category]++; + } + }); + + next(); +}); + +// Instance methods +wardrobeSchema.methods.addItem = function(itemData) { + this.items.push(itemData); + return this.save(); +}; + +wardrobeSchema.methods.removeItem = function(itemId) { + this.items = this.items.filter(item => item._id.toString() !== itemId); + return this.save(); +}; + +wardrobeSchema.methods.updateItem = function(itemId, updateData) { + const item = this.items.id(itemId); + if (item) { + Object.assign(item, updateData); + item.updatedAt = new Date(); + return this.save(); + } + throw new Error('Item not found'); +}; + +wardrobeSchema.methods.getItemsByCategory = function(category) { + return this.items.filter(item => item.category === category && item.isActive); +}; + +wardrobeSchema.methods.getItemsByTags = function(tags) { + return this.items.filter(item => + item.isActive && + tags.some(tag => + item.aiTags.includes(tag) || + item.userTags.includes(tag) + ) + ); +}; + +wardrobeSchema.methods.generateOutfitRecommendations = function() { + // This would integrate with the recommendation service + const tops = this.getItemsByCategory('tops'); + const bottoms = this.getItemsByCategory('bottoms'); + const shoes = this.getItemsByCategory('shoes'); + const accessories = this.getItemsByCategory('accessories'); + + const recommendations = []; + + // Simple outfit combination logic + for (let i = 0; i < Math.min(3, tops.length); i++) { + for (let j = 0; j < Math.min(3, bottoms.length); j++) { + for (let k = 0; k < Math.min(2, shoes.length); k++) { + recommendations.push({ + id: `${tops[i]._id}-${bottoms[j]._id}-${shoes[k]._id}`, + items: [tops[i], bottoms[j], shoes[k]], + confidence: Math.random(), // This would come from AI analysis + }); + } + } + } + + return recommendations.slice(0, 10); // Limit to 10 recommendations +}; + +// Static methods +wardrobeSchema.statics.findByUserId = function(userId) { + return this.findOne({ userId }); +}; + +wardrobeSchema.statics.findPublicWardrobes = function(limit = 20, skip = 0) { + return this.find({ isPublic: true }) + .limit(limit) + .skip(skip) + .sort({ updatedAt: -1 }); +}; + +module.exports = mongoose.model('Wardrobe', wardrobeSchema); diff --git a/src/routes/admin.js b/src/routes/admin.js new file mode 100644 index 0000000..33ffe9e --- /dev/null +++ b/src/routes/admin.js @@ -0,0 +1,328 @@ +// const express = require('express'); +// const multer = require('multer'); +// const { prisma } = require('../config/database'); +// const Product = require('../models/mongodb/Product'); +// const { protect, authorize } = require('../middleware/auth'); + +// const dashboard = require('../controllers/admin/dashboardController'); +// const users = require('../controllers/admin/userController'); +// const orders = require('../controllers/admin/orderController'); +// const products = require('../controllers/admin/productController'); +// const categories = require('../controllers/admin/categoryController'); +// const coupons = require('../controllers/admin/couponController'); + +// const router = express.Router(); + +// // โœ… FIXED: Use multer().any() to accept dynamic field names +// const upload = multer({ +// storage: multer.memoryStorage(), +// limits: { +// fileSize: 5 * 1024 * 1024, // 5MB limit per file +// }, +// }); + +// // All routes require admin authentication +// router.use(protect); +// router.use(authorize('ADMIN')); + +// // @desc Get dashboard statistics +// // @route GET /api/admin/dashboard +// // @access Private/Admin +// router.get('/dashboard', dashboard.getDashboardStats); + +// /** +// * @desc Get coupon statistics +// * @route GET /api/coupons/admin/stats +// * @access Private/Admin +// */ +// router.get( +// '/stats', +// protect, +// authorize('ADMIN', 'SUPER_ADMIN'), +// coupons.getCouponStats +// ); + +// // @desc Get all users with pagination +// // @route GET /api/admin/users +// // @access Private/Admin +// router.get('/users', users.getAllUsers); + +// router.get('/users/:id', users.getUserById); +// // @desc Update user status +// // @route PUT /api/admin/users/:id/status +// // @access Private/Admin +// router.put('/users/:id/status', users.updateUserStatus); + +// // @desc Get all orders with filters +// // @route GET /api/admin/orders +// // @access Private/Admin +// router.get('/orders', orders.getAllOrders); + +// /** +// * @desc Get status change statistics +// * @route GET /api/admin/orders/stats/status-changes +// * @access Private/Admin +// */ +// router.get( +// '/stats/status-changes', +// protect, +// authorize('ADMIN', 'SUPER_ADMIN'), +// orders.getStatusChangeStats +// ); + +// /** +// * @desc Get single order with full history +// * @route GET /api/admin/orders/:orderId +// * @access Private/Admin +// */ +// router.get( +// '/:orderId', +// protect, +// authorize('ADMIN', 'SUPER_ADMIN'), +// orders.getOrderById +// ); + +// /** +// * @desc Get order status history +// * @route GET /api/admin/orders/:orderId/history +// * @access Private/Admin +// */ +// router.get( +// '/:orderId/history', +// protect, +// authorize('ADMIN', 'SUPER_ADMIN'), +// orders.getOrderStatusHistory +// ); + +// //Order Details Page +// router.get('/orders/:id', orders.getOrderDetails); + +// // @desc Get all products +// // @route GET /api/admin/products +// // @access Private/Admin +// router.get('/products', products.getAllProducts); + +// // @desc Create new product +// // @route POST /api/admin/products +// // @access Private/Admin +// // Create Product Route +// router.post( +// '/products', +// protect, +// authorize('ADMIN', 'SUPER_ADMIN'), +// upload.any(), // โœ… This accepts ANY field names (including dynamic variant fields) +// products.createProduct +// ); + +// /** +// * @desc Get all coupons +// * @route GET /api/coupons/admin +// * @access Private/Admin +// */ +// router.get( +// '/coupons', +// protect, +// authorize('ADMIN', 'SUPER_ADMIN'), +// coupons.getAllCoupons +// ); + +// // @desc Update product +// // @route PUT /api/admin/products/:id +// // @access Private/Admin +// router.put('/products/:id', products.updateProduct); + +// // @desc Delete product +// // @route DELETE /api/admin/products/:id +// // @access Private/Admin +// router.delete('/products/:id', products.deleteProduct); + +// // @desc Get all categories +// // @route GET /api/admin/categories +// // @access Private/Admin +// router.get('/categories', categories.getAllCategories); + +// // @desc Create new category +// // @route POST /api/admin/categories +// // @access Private/Admin +// router.post('/categories', categories.createCategory); + +// router.put('/categories/:id', categories.updateCategory); + +// router.delete('/categories/:id', categories.deleteCategory); + +// router.patch('/categories/:id/status', categories.toggleCategoryStatus); + +// router.patch('/categories/reorder', categories.reorderCategories); + +// router.get('/categories/:id', categories.getCategoryById); + +// // Category tree / hierarchy +// router.get('/tree', categories.getCategoryHierarchy); + +// // @desc Get all coupons +// // @route GET /api/admin/coupons +// // @access Private/Admin +// // router.get('/coupons', coupons.getAllCoupons); + +// // @desc Create new coupon +// // @route POST /api/admin/coupons +// // @access Private/Admin +// // router.post('/coupons', coupons.createCoupon); + +// // ========================================== +// // ADMIN ROUTES +// // ========================================== + +// /** +// * @desc Get single coupon +// * @route GET /api/coupons/admin/:id +// * @access Private/Admin +// */ +// router.get( +// '/coupons/:id', +// protect, +// authorize('ADMIN', 'SUPER_ADMIN'), +// coupons.getCouponById +// ); + +// /** +// * @desc Create coupon +// * @route POST /api/coupons/admin +// * @access Private/Admin +// */ +// router.post( +// '/coupons', +// protect, +// authorize('ADMIN', 'SUPER_ADMIN'), +// coupons.createCoupon +// ); + +// /** +// * @desc Update coupon +// * @route PUT /api/coupons/admin/:id +// * @access Private/Admin +// */ +// router.put( +// '/coupons/:id', +// protect, +// authorize('ADMIN', 'SUPER_ADMIN'), +// coupons.updateCoupon +// ); + +// /** +// * @desc Delete coupon +// * @route DELETE /api/coupons/admin/:id +// * @access Private/Admin +// */ +// router.delete( +// '/coupons/:id', +// protect, +// authorize('ADMIN', 'SUPER_ADMIN'), +// coupons.deleteCoupon +// ); + +// /** +// * @desc Toggle coupon status +// * @route PATCH /api/coupons/admin/:id/toggle +// * @access Private/Admin +// */ +// router.patch( +// '/coupons/:id/toggle', +// protect, +// authorize('ADMIN', 'SUPER_ADMIN'), +// coupons.toggleCouponStatus +// ); + +// module.exports = router; + +// routes/adminRoutes.js - FIXED VERSION with correct route ordering + +const express = require('express'); +const multer = require('multer'); +const { prisma } = require('../config/database'); +const Product = require('../models/mongodb/Product'); +const { protect, authorize } = require('../middleware/auth'); + +const dashboard = require('../controllers/admin/dashboardController'); +const users = require('../controllers/admin/userController'); +const orders = require('../controllers/admin/orderController'); +const products = require('../controllers/admin/productController'); +const categories = require('../controllers/admin/categoryController'); +const coupons = require('../controllers/admin/couponController'); + +const router = express.Router(); + +const upload = multer({ + storage: multer.memoryStorage(), + limits: { + fileSize: 5 * 1024 * 1024, + }, +}); + +// All routes require admin authentication +router.use(protect); +router.use(authorize('ADMIN')); + +// ========================================== +// DASHBOARD +// ========================================== +router.get('/dashboard', dashboard.getDashboardStats); + +// ========================================== +// USERS +// ========================================== +router.get('/users', users.getAllUsers); +router.get('/users/:id', users.getUserById); +router.put('/users/:id/status', users.updateUserStatus); + +// ========================================== +// ORDERS - SPECIFIC ROUTES FIRST +// ========================================== +router.get('/orders/stats/status-changes', orders.getStatusChangeStats); +router.get('/orders', orders.getAllOrders); +router.get('/orders/:id', orders.getOrderDetails); +router.get('/orders/:orderId/history', orders.getOrderStatusHistory); +// router.put('/orders/:orderId/status', orders.updateOrderStatus); + +// ========================================== +// PRODUCTS +// ========================================== +router.get('/products', products.getAllProducts); +router.post('/products', upload.any(), products.createProduct); +router.put('/products/:id', products.updateProduct); +router.delete('/products/:id', products.deleteProduct); + +// ========================================== +// CATEGORIES - SPECIFIC ROUTES FIRST +// ========================================== +router.get('/tree', categories.getCategoryHierarchy); +router.patch('/categories/reorder', categories.reorderCategories); +router.get('/categories', categories.getAllCategories); +// router.post('/categories', categories.createCategory); +router.post( + '/categories', + upload.single('image'), // ๐Ÿ‘ˆ THIS IS REQUIRED + categories.createCategory +); +router.get('/categories/:id', categories.getCategoryById); +// router.put('/categories/:id', categories.updateCategory); +router.put( + '/categories/:id', + upload.single('image'), // ๐Ÿ‘ˆ important + categories.updateCategory +); +router.delete('/categories/:id', categories.deleteCategory); +router.patch('/categories/:id/status', categories.toggleCategoryStatus); + +// ========================================== +// COUPONS - SPECIFIC ROUTES FIRST +// ========================================== +router.get('/stats', coupons.getCouponStats); +router.get('/coupons', coupons.getAllCoupons); +router.post('/coupons', coupons.createCoupon); +router.get('/coupons/:id', coupons.getCouponById); +router.put('/coupons/:id', coupons.updateCoupon); +router.delete('/coupons/:id', coupons.deleteCoupon); +router.patch('/coupons/:id/toggle', coupons.toggleCouponStatus); + +module.exports = router; diff --git a/src/routes/auth.js b/src/routes/auth.js new file mode 100644 index 0000000..ea9eb4d --- /dev/null +++ b/src/routes/auth.js @@ -0,0 +1,68 @@ +const express = require('express'); +const authService = require('../services/authService'); +const { protect } = require('../middleware/auth'); +const authController = require('../controllers/authController'); +const router = express.Router(); + +// @desc Register user +// @route POST /api/auth/register +// @access Public +router.post('/register', authController.register); + +// @desc Login user +// @route POST /api/auth/login +// @access Public +router.post('/login', authController.login); + + +// @desc Send OTP to WhatsApp +// @route POST /api/auth/send-otp +// @access Public +router.post('/send-otp', authController.sendLoginOTP); + +// @desc Verify OTP and login +// @route POST /api/auth/verify-otp +// @access Public +router.post('/verify-otp', authController.verifyLoginOTP); + +// @desc Refresh token +// @route POST /api/auth/refresh +// @access Public +router.post('/refresh', authController.refreshToken); + +// @desc Logout user +// @route POST /api/auth/logout +// @access Private +router.post('/logout', protect, authController.logout); + +// @desc Change password +// @route PUT /api/auth/change-password +// @access Private +router.put('/change-password', protect, authController.changePassword); + +// @desc Request password reset +// @route POST /api/auth/forgot-password +// @access Public +router.post('/forgot-password', authController.forgotPassword); + +// @desc Reset password with token +// @route POST /api/auth/reset-password +// @access Public +router.post('/reset-password', authController.resetPassword); + +// @desc Send verification email +// @route POST /api/auth/send-verification +// @access Private +router.post('/send-verification', protect, authController.sendVerification); + +// @desc Verify email with token +// @route POST /api/auth/verify-email +// @access Public +router.post('/verify-email', authController.verifyEmail); + +// @desc Get current user profile +// @route GET /api/auth/me +// @access Private +router.get('/me', protect, authController.getMe); + +module.exports = router; diff --git a/src/routes/couponRoutes.js b/src/routes/couponRoutes.js new file mode 100644 index 0000000..d3204cb --- /dev/null +++ b/src/routes/couponRoutes.js @@ -0,0 +1,40 @@ +const express = require('express'); +const { protect, authorize, optionalAuth } = require('../middleware/auth'); +// const adminCouponController = require('../controllers/admin/couponController'); +const userCouponController = require('../controllers/couponController'); + +const router = express.Router(); + +// ========================================== +// USER ROUTES (Public/Customer) +// ========================================== + +/** + * @desc Get available coupons + * @route GET /api/coupons/available + * @access Public + */ +router.get('/available', userCouponController.getAvailableCoupons); + +/** + * @desc Validate coupon code + * @route POST /api/coupons/validate + * @access Public + */ +router.post('/validate', optionalAuth, userCouponController.validateCoupon); + +/** + * @desc Apply coupon to order + * @route POST /api/coupons/apply + * @access Private + */ +router.post('/apply', protect, userCouponController.applyCouponToOrder); + +/** + * @desc Remove coupon from order + * @route POST /api/coupons/remove + * @access Private + */ +router.post('/remove', protect, userCouponController.removeCouponFromOrder); + +module.exports = router; diff --git a/src/routes/deliveryRoutes.js b/src/routes/deliveryRoutes.js new file mode 100644 index 0000000..d316db8 --- /dev/null +++ b/src/routes/deliveryRoutes.js @@ -0,0 +1,35 @@ +// routes/deliveryRoutes.js + +const express = require('express'); +const { protect, authorize } = require('../middleware/auth'); +const trackingController = require('../controllers/orderTrackingController'); + +const router = express.Router(); + +/** + * @desc Get delivery estimation for pincode + * @route POST /api/delivery/estimate + * @access Public + */ +router.post('/estimate', trackingController.getDeliveryEstimate); + +/** + * @desc Get order tracking details + * @route GET /api/orders/:orderId/tracking + * @access Private + */ +router.get('/orders/:orderId/tracking', protect, trackingController.getOrderTracking); + +/** + * @desc Update order status (Admin) + * @route PUT /api/admin/orders/:orderId/status + * @access Private/Admin + */ +router.put( + '/admin/:orderId/status', + protect, + authorize('ADMIN', 'SUPER_ADMIN'), + trackingController.updateOrderStatus +); + +module.exports = router; \ No newline at end of file diff --git a/src/routes/orders.js b/src/routes/orders.js new file mode 100644 index 0000000..8c3cd78 --- /dev/null +++ b/src/routes/orders.js @@ -0,0 +1,100 @@ +const express = require('express'); +const { prisma } = require('../config/database'); +const { protect, authorize } = require('../middleware/auth'); +const orderController = require('../controllers/orderController'); + +const router = express.Router(); + +// @desc Create new order +// @route POST /api/orders +// @access Private +router.post('/', protect, orderController.createOrder); + +// @desc Get user orders +// @route GET /api/orders +// @access Private +router.get('/', protect, orderController.getUserOrders); + +// @desc Get single order +// @route GET /api/orders/:id +// @access Private +router.get('/:id', protect, orderController.getOrderById); + +// @desc Update order status (Admin only) +// @route PUT /api/orders/:id/status +// @access Private/Admin +router.put( + '/:id/status', + protect, + authorize('ADMIN'), + orderController.updateOrderStatus +); + +// @desc Cancel order +// @route PUT /api/orders/:id/cancel +// @access Private +router.put('/:id/cancel', protect, orderController.cancelOrder); + +// @desc Return order +// @route PUT /api/orders/:id/return +// @access Private +router.put('/:id/return', protect, orderController.returnOrder); + + + +// @desc Get all orders (Admin only) +// @route GET /api/orders/admin/all +// @access Private/Admin +router.get( + '/admin/all', + protect, + authorize('ADMIN'), + orderController.getAllOrdersAdmin +); + +// Admin approve/reject return +router.put( + '/:id/return/status', + protect, + authorize('ADMIN'), + orderController.updateReturnStatus +); + + +// Admin: list all return requests +// router.get( +// '/admin/returns', +// protect, +// authorize('ADMIN'), +// orderController.getReturnRequestsAdmin +// ); + +// Admin: list all return requests +router.get( + '/admin/returns', + protect, + authorize('ADMIN'), + orderController.getAdminReturnRequests +); + + + +// Admin: list all returned products (approved/completed) +router.get( + '/admin/returns/list', + protect, + authorize('ADMIN'), + orderController.getReturnedProducts +); + + +// Get single return request details +router.get( + '/admin/returns/:id', + protect, + authorize('ADMIN'), + orderController.getReturnRequestById +); + + +module.exports = router; diff --git a/src/routes/paymentRoutes.js b/src/routes/paymentRoutes.js new file mode 100644 index 0000000..bb56bfd --- /dev/null +++ b/src/routes/paymentRoutes.js @@ -0,0 +1,52 @@ +// routes/paymentRoutes.js +const express = require('express'); +const paytmController = require('../controllers/payment/paytmController'); +const { protect, authorize } = require('../middleware/auth'); + +const router = express.Router(); + +// ====================== +// PAYTM PAYMENT ROUTES +// ====================== + +/** + * @desc Initiate Paytm Payment + * @route POST /api/payments/paytm/initiate + * @access Private + */ +router.post('/paytm/initiate', protect, paytmController.initiatePayment); + +/** + * @desc Paytm Payment Callback (Called by Paytm after payment) + * @route POST /api/payments/paytm/callback + * @access Public (No auth - Paytm calls this) + */ +router.post('/paytm/callback', paytmController.paymentCallback); + +/** + * @desc Check Payment Status + * @route GET /api/payments/paytm/status/:orderId + * @access Private + */ +router.get('/paytm/status/:orderId', protect, paytmController.checkPaymentStatus); + +/** + * @desc Get Payment Details + * @route GET /api/payments/paytm/:orderId + * @access Private + */ +router.get('/paytm/:orderId', protect, paytmController.getPaymentDetails); + +/** + * @desc Process Refund (Admin only) + * @route POST /api/payments/paytm/refund + * @access Private/Admin + */ +router.post( + '/paytm/refund', + protect, + authorize('ADMIN', 'SUPER_ADMIN'), + paytmController.processRefund +); + +module.exports = router; \ No newline at end of file diff --git a/src/routes/products.js b/src/routes/products.js new file mode 100644 index 0000000..4b7367b --- /dev/null +++ b/src/routes/products.js @@ -0,0 +1,137 @@ +const express = require('express'); +const Product = require('../models/mongodb/Product'); +const { protect, authorize, optionalAuth } = require('../middleware/auth'); +const productController = require('../controllers/products/productController'); +const recommendationController = require('../controllers/products/recommendation'); +const router = express.Router(); + + + +/** + * @desc Get personalized recommendations for user + * @route GET /api/products/recommendations/personalized + * @access Private/Public + */ +router.get( + '/recommendations/personalized', + optionalAuth, + recommendationController.getPersonalizedRecommendations +); + +// SPECIFIC ROUTES FIRST (before /:slug) +// @desc Get products by category +// @route GET /api/products/category/:category +// @access Public +router.get('/categories', productController.getAllCategories); + +router.get('/debug-categories', productController.debugMissingCategories); + +router.get('/tree', productController.getUserCategoryHierarchy); + +// @desc Get available colors/variants for a category +// @route GET /api/products/category-colors/:categorySlug +// @access Public +router.get( + '/category-colors/:categorySlug', + optionalAuth, + productController.getCategoryColors +); + +// @desc Get featured products +// @route GET /api/products/featured +// @access Public +router.get('/featured', optionalAuth, productController.getFeaturedProducts); + +// @desc Search products +// @route GET /api/products/search/:query +// @access Public +router.get('/search/:query', optionalAuth, productController.searchProducts); + +router.get('/most-loved', productController.getMostLovedProducts); + +router.get('/new-arrivals', productController.getNewArrivals); + +router.get( + '/category/:categorySlug', + optionalAuth, + productController.getProductsByCategory +); + +// @desc Get all products +// @route GET /api/products +// @access Public +router.get('/', optionalAuth, productController.getAllProducts); + + +// ====================== +// PRODUCT-SPECIFIC RECOMMENDATION ROUTES (BEFORE /:slug) +// ====================== + +/** + * @desc Get recommended products for a specific product + * @route GET /api/products/:slug/recommendations + * @access Public + */ +router.get( + '/:slug/recommendations', + optionalAuth, + recommendationController.getProductRecommendations +); + +/** + * @desc Get "Customers also bought" products + * @route GET /api/products/:slug/also-bought + * @access Public + */ +router.get( + '/:slug/also-bought', + optionalAuth, + recommendationController.getAlsoBoughtProducts +); + +/** + * @desc Get similar products + * @route GET /api/products/:slug/similar + * @access Public + */ +router.get( + '/:slug/similar', + optionalAuth, + recommendationController.getSimilarProducts +); + +// @desc Get single product +// @route GET /api/products/:slug +// @access Public +router.get('/:slug', optionalAuth, productController.getProductBySlug); + + + +// ADMIN ROUTES +// @desc Create new product +// @route POST /api/products +// @access Private/Admin +router.post('/', protect, authorize('ADMIN'), productController.createProduct); + +// @desc Update product +// @route PUT /api/products/:id +// @access Private/Admin +router.put( + '/:id', + protect, + authorize('ADMIN'), + productController.updateProduct +); + +// @desc Delete product +// @route DELETE /api/products/:id +// @access Private/Admin +router.delete( + '/:id', + protect, + authorize('ADMIN'), + productController.deleteProduct +); + + +module.exports = router; diff --git a/src/routes/reports.js b/src/routes/reports.js new file mode 100644 index 0000000..371ec30 --- /dev/null +++ b/src/routes/reports.js @@ -0,0 +1,85 @@ +// const express = require("express"); +// const { protect, authorize } = require("../middleware/auth"); + +// const reports = require("../controllers/admin/reportController"); + +// const router = express.Router(); + +// router.use(protect); +// router.use(authorize("ADMIN")); + +// // Reports Endpoints +// router.get("/overview", reports.getOverviewReport); +// router.get("/sales", reports.getSalesAnalytics); +// router.get("/customers", reports.getCustomerStats); +// router.get("/sellers", reports.getSellerStats); +// router.get("/orders", reports.getOrderAnalytics); + +// router.get("/inventory", reports.getInventoryStats); +// router.get("/financial", reports.getFinancialStats); + + +// module.exports = router; + + + +const express = require('express'); +const { protect, authorize } = require('../middleware/auth'); +const reports = require('../controllers/admin/reportController'); + +const router = express.Router(); + +// โ”€โ”€โ”€ Auth Guard โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +router.use(protect); +router.use(authorize('ADMIN')); + +// โ”€โ”€โ”€ Overview โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +// GET /api/admin/reports/overview?from=2024-01-01&to=2024-12-31 +router.get('/overview', reports.getOverviewReport); + +// โ”€โ”€โ”€ Sales โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +// GET /api/admin/reports/sales?from=&to=&groupBy=daily|monthly +router.get('/sales', reports.getSalesAnalytics); + +// โ”€โ”€โ”€ Customers โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +// GET /api/admin/reports/customers?view=weekly|monthly|yearly +router.get('/customers', reports.getCustomerStats); + +// โ”€โ”€โ”€ Sellers โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +// GET /api/admin/reports/sellers?page=1&limit=20 +router.get('/sellers', reports.getSellerStats); + +// โ”€โ”€โ”€ Orders โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +// GET /api/admin/reports/orders?from=&to= +router.get('/orders', reports.getOrderAnalytics); + +// โ”€โ”€โ”€ Inventory โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +// GET /api/admin/reports/inventory?page=1&limit=20 +router.get('/inventory', reports.getInventoryStats); + +// โ”€โ”€โ”€ Financial โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +// GET /api/admin/reports/financial?from=&to= +router.get('/financial', reports.getFinancialStats); + +// โ”€โ”€โ”€ Payouts โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +// GET /api/admin/reports/payouts?page=1&limit=20&from=&to= +router.get('/payouts', reports.getPayoutHistory); + +// โ”€โ”€โ”€ Activity Feed โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +// GET /api/admin/reports/activity?limit=20 +// Requires ActivityLog model in Prisma schema (see note in controller) +router.get('/activity', reports.getActivityFeed); + +// โ”€โ”€โ”€ Top Products โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +// GET /api/admin/reports/top-products?limit=10 +router.get('/top-products', reports.getTopProducts); + +// โ”€โ”€โ”€ Coupon Stats โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +// GET /api/admin/reports/coupons?from=&to= +router.get('/coupons', reports.getCouponStats); + +// โ”€โ”€โ”€ Returns (uncomment when ReturnModel is ready) โ”€โ”€โ”€ +// GET /api/admin/reports/returns?from=&to= +// router.get('/returns', reports.getReturnAnalytics); + +module.exports = router; \ No newline at end of file diff --git a/src/routes/upload.routes.js b/src/routes/upload.routes.js new file mode 100644 index 0000000..53e44e8 --- /dev/null +++ b/src/routes/upload.routes.js @@ -0,0 +1,17 @@ +const express = require("express"); +const multer = require("multer"); +const { uploadToS3 } = require("../services/s3Upload.service.js"); + +const router = express.Router(); +const upload = multer({ dest: "uploads/" }); + +router.post("/upload", upload.single("image"), async (req, res) => { + try { + const imageUrl = await uploadToS3(req.file); + res.json({ success: true, imageUrl }); + } catch (err) { + res.status(500).json({ error: err.message }); + } +}); + +module.exports = router; diff --git a/src/routes/users.js b/src/routes/users.js new file mode 100644 index 0000000..b387e5e --- /dev/null +++ b/src/routes/users.js @@ -0,0 +1,550 @@ +const express = require('express'); +const { prisma } = require('../config/database'); +const { protect, authorize, checkOwnership } = require('../middleware/auth'); + +const profile = require('../controllers/users/profileController'); +const address = require('../controllers/users/addressController'); +// const orders = require('../controllers/users/orderController'); +const orders = require('../controllers/users/orderController'); +const wishlist = require('../controllers/users/wishlistController'); +const adminUsers = require('../controllers/users/adminUserController'); +const cart = require('../controllers/users/cartController'); +// import { uploadProfile } from "../middleware/uploadProfile"; +const { uploadProfile } = require('../middleware/uploadProfile'); +const router = express.Router(); + +// @desc Get user profile +// @route GET /api/users/profile +// @access Private +// router.get('/profile', protect, async (req, res, next) => { +// try { +// const user = await prisma.user.findUnique({ +// where: { id: req.user.id }, +// select: { +// id: true, +// email: true, +// username: true, +// firstName: true, +// lastName: true, +// phone: true, +// avatar: true, +// role: true, +// isVerified: true, +// createdAt: true, +// lastLoginAt: true, +// }, +// }); + +// res.json({ +// success: true, +// data: { user }, +// }); +// } catch (error) { +// next(error); +// } +// }); + +router.get('/profile', protect, profile.getProfile); + +// @desc Update user profile +// @route PUT /api/users/profile +// @access Private +// router.put('/profile', protect, async (req, res, next) => { +// try { +// const { firstName, lastName, username, phone } = req.body; + +// // Check if username is already taken +// if (username) { +// const existingUser = await prisma.user.findFirst({ +// where: { +// username, +// NOT: { id: req.user.id }, +// }, +// }); + +// if (existingUser) { +// return res.status(400).json({ +// success: false, +// message: 'Username already taken', +// }); +// } +// } + +// const updatedUser = await prisma.user.update({ +// where: { id: req.user.id }, +// data: { +// firstName, +// lastName, +// username, +// phone, +// }, +// select: { +// id: true, +// email: true, +// username: true, +// firstName: true, +// lastName: true, +// phone: true, +// avatar: true, +// role: true, +// isVerified: true, +// }, +// }); + +// res.json({ +// success: true, +// message: 'Profile updated successfully', +// data: { user: updatedUser }, +// }); +// } catch (error) { +// next(error); +// } +// }); + +router.put('/profile', protect, profile.updateProfile); + +// @desc Upload user avatar +// @route POST /api/users/avatar +// @access Private +// router.post('/avatar', protect, async (req, res, next) => { +// try { +// // This would integrate with file upload middleware +// // For now, just return a placeholder +// res.json({ +// success: true, +// message: 'Avatar upload endpoint - to be implemented with file upload middleware', +// }); +// } catch (error) { +// next(error); +// } +// }); + +// router.post('/avatar', protect, profile.uploadAvatar); +router.post("/avatar", protect, uploadProfile.single("avatar"), profile.uploadAvatar); + + +// @desc Get user addresses +// @route GET /api/users/addresses +// @access Private +// router.get('/addresses', protect, async (req, res, next) => { +// try { +// const addresses = await prisma.address.findMany({ +// where: { userId: req.user.id }, +// orderBy: { createdAt: 'desc' }, +// }); + +// res.json({ +// success: true, +// data: { addresses }, +// }); +// } catch (error) { +// next(error); +// } +// }); + +router.get('/addresses', protect, address.getAddresses); + +// @desc Add user address +// @route POST /api/users/addresses +// @access Private +// router.post('/addresses', protect, async (req, res, next) => { +// try { +// const { +// type, +// isDefault, +// firstName, +// lastName, +// company, +// addressLine1, +// addressLine2, +// city, +// state, +// postalCode, +// country, +// phone, +// } = req.body; + +// // If this is set as default, unset other default addresses +// if (isDefault) { +// await prisma.address.updateMany({ +// where: { userId: req.user.id }, +// data: { isDefault: false }, +// }); +// } + +// const address = await prisma.address.create({ +// data: { +// userId: req.user.id, +// type, +// isDefault, +// firstName, +// lastName, +// company, +// addressLine1, +// addressLine2, +// city, +// state, +// postalCode, +// country, +// phone, +// }, +// }); + +// res.status(201).json({ +// success: true, +// message: 'Address added successfully', +// data: { address }, +// }); +// } catch (error) { +// next(error); +// } +// }); + +router.post('/addresses', protect, address.addAddress); + +// @desc Update user address +// @route PUT /api/users/addresses/:id +// @access Private +// router.put('/addresses/:id', protect, async (req, res, next) => { +// try { +// const addressId = req.params.id; +// const { +// type, +// isDefault, +// firstName, +// lastName, +// company, +// addressLine1, +// addressLine2, +// city, +// state, +// postalCode, +// country, +// phone, +// } = req.body; + +// // Check if address belongs to user +// const existingAddress = await prisma.address.findFirst({ +// where: { +// id: addressId, +// userId: req.user.id, +// }, +// }); + +// if (!existingAddress) { +// return res.status(404).json({ +// success: false, +// message: 'Address not found', +// }); +// } + +// // If this is set as default, unset other default addresses +// if (isDefault) { +// await prisma.address.updateMany({ +// where: { userId: req.user.id }, +// data: { isDefault: false }, +// }); +// } + +// const updatedAddress = await prisma.address.update({ +// where: { id: addressId }, +// data: { +// type, +// isDefault, +// firstName, +// lastName, +// company, +// addressLine1, +// addressLine2, +// city, +// state, +// postalCode, +// country, +// phone, +// }, +// }); + +// res.json({ +// success: true, +// message: 'Address updated successfully', +// data: { address: updatedAddress }, +// }); +// } catch (error) { +// next(error); +// } +// }); + +router.put('/addresses/:id', protect, address.updateAddress); + +// @desc Delete user address +// @route DELETE /api/users/addresses/:id +// @access Private +// router.delete('/addresses/:id', protect, async (req, res, next) => { +// try { +// const addressId = req.params.id; + +// // Check if address belongs to user +// const existingAddress = await prisma.address.findFirst({ +// where: { +// id: addressId, +// userId: req.user.id, +// }, +// }); + +// if (!existingAddress) { +// return res.status(404).json({ +// success: false, +// message: 'Address not found', +// }); +// } + +// await prisma.address.delete({ +// where: { id: addressId }, +// }); + +// res.json({ +// success: true, +// message: 'Address deleted successfully', +// }); +// } catch (error) { +// next(error); +// } +// }); + +router.delete('/addresses/:id', protect, address.deleteAddress); + +// @desc Get user orders +// @route GET /api/users/orders +// @access Private +// router.get('/orders', protect, async (req, res, next) => { +// try { +// const { page = 1, limit = 10, status } = req.query; +// const skip = (page - 1) * limit; + +// const where = { userId: req.user.id }; +// if (status) { +// where.status = status; +// } + +// const [orders, total] = await Promise.all([ +// prisma.order.findMany({ +// where, +// include: { +// items: true, +// address: true, +// }, +// orderBy: { createdAt: 'desc' }, +// skip: parseInt(skip), +// take: parseInt(limit), +// }), +// prisma.order.count({ where }), +// ]); + +// res.json({ +// success: true, +// data: { +// orders, +// pagination: { +// page: parseInt(page), +// limit: parseInt(limit), +// total, +// pages: Math.ceil(total / limit), +// }, +// }, +// }); +// } catch (error) { +// next(error); +// } +// }); + +router.get('/orders', protect, orders.getOrders); + +// @desc Get user wishlist +// @route GET /api/users/wishlist +// @access Private +// router.get('/wishlist', protect, async (req, res, next) => { +// try { +// const wishlistItems = await prisma.wishlistItem.findMany({ +// where: { userId: req.user.id }, +// orderBy: { createdAt: 'desc' }, +// }); + +// res.json({ +// success: true, +// data: { wishlist: wishlistItems }, +// }); +// } catch (error) { +// next(error); +// } +// }); + +router.get('/wishlist', protect, wishlist.getWishlist); + +// @desc Add item to wishlist +// @route POST /api/users/wishlist +// @access Private +// router.post('/wishlist', protect, async (req, res, next) => { +// try { +// const { productId } = req.body; + +// if (!productId) { +// return res.status(400).json({ +// success: false, +// message: 'Product ID is required', +// }); +// } + +// // Check if item already exists in wishlist +// const existingItem = await prisma.wishlistItem.findUnique({ +// where: { +// userId_productId: { +// userId: req.user.id, +// productId, +// }, +// }, +// }); + +// if (existingItem) { +// return res.status(400).json({ +// success: false, +// message: 'Item already in wishlist', +// }); +// } + +// const wishlistItem = await prisma.wishlistItem.create({ +// data: { +// userId: req.user.id, +// productId, +// }, +// }); + +// res.status(201).json({ +// success: true, +// message: 'Item added to wishlist', +// data: { wishlistItem }, +// }); +// } catch (error) { +// next(error); +// } +// }); + +router.post('/wishlist', protect, wishlist.addToWishlist); + +// @desc Remove item from wishlist +// @route DELETE /api/users/wishlist/:productId +// @access Private +// router.delete('/wishlist/:productId', protect, async (req, res, next) => { +// try { +// const { productId } = req.params; + +// const wishlistItem = await prisma.wishlistItem.findUnique({ +// where: { +// userId_productId: { +// userId: req.user.id, +// productId, +// }, +// }, +// }); + +// if (!wishlistItem) { +// return res.status(404).json({ +// success: false, +// message: 'Item not found in wishlist', +// }); +// } + +// await prisma.wishlistItem.delete({ +// where: { +// userId_productId: { +// userId: req.user.id, +// productId, +// }, +// }, +// }); + +// res.json({ +// success: true, +// message: 'Item removed from wishlist', +// }); +// } catch (error) { +// next(error); +// } +// }); + +router.delete('/wishlist/:productId', protect, wishlist.removeFromWishlist); + +// @desc Get all users (Admin only) +// @route GET /api/users +// @access Private/Admin +// router.get('/', protect, authorize('ADMIN'), async (req, res, next) => { +// try { +// const { page = 1, limit = 20, role, search } = req.query; +// const skip = (page - 1) * limit; + +// const where = {}; +// if (role) where.role = role; +// if (search) { +// where.OR = [ +// { email: { contains: search, mode: 'insensitive' } }, +// { firstName: { contains: search, mode: 'insensitive' } }, +// { lastName: { contains: search, mode: 'insensitive' } }, +// { username: { contains: search, mode: 'insensitive' } }, +// ]; +// } + +// const [users, total] = await Promise.all([ +// prisma.user.findMany({ +// where, +// select: { +// id: true, +// email: true, +// username: true, +// firstName: true, +// lastName: true, +// role: true, +// isVerified: true, +// isActive: true, +// createdAt: true, +// lastLoginAt: true, +// }, +// orderBy: { createdAt: 'desc' }, +// skip: parseInt(skip), +// take: parseInt(limit), +// }), +// prisma.user.count({ where }), +// ]); + +// res.json({ +// success: true, +// data: { +// users, +// pagination: { +// page: parseInt(page), +// limit: parseInt(limit), +// total, +// pages: Math.ceil(total / limit), +// }, +// }, +// }); +// } catch (error) { +// next(error); +// } +// }); +router.get('/', protect, authorize('ADMIN'), adminUsers.getAllUsers); + +// Get user's shopping cart +router.get('/my-cart', protect, cart.getCart); + +// Add product to cart +router.post('/my-cart/add-item', protect, cart.addToCart); + +// Update item quantity in cart +router.put('/my-cart/update-item/:productId', protect, cart.updateQuantity); + +// Remove item from cart +router.delete('/my-cart/remove-item/:productId', protect, cart.removeFromCart); + +// Clear all items from cart +// router.delete('/my-cart/clear', protect, cart.clearCart); + +module.exports = router; diff --git a/src/routes/wardrobe.js b/src/routes/wardrobe.js new file mode 100644 index 0000000..9df9257 --- /dev/null +++ b/src/routes/wardrobe.js @@ -0,0 +1,393 @@ +const express = require('express'); +const Wardrobe = require('../models/mongodb/Wardrobe'); +const { protect, authorize } = require('../middleware/auth'); + + +const { getWardrobe, updateWardrobe } = require('../controllers/wardrobe/wardrobeMainController'); +const { addItem, updateItem, removeItem, getItemsByCategory } = require('../controllers/wardrobe/wardrobeItemController'); +const { searchItems } = require('../controllers/wardrobe/wardrobeSearchController'); +const { getRecommendations } = require('../controllers/wardrobe/wardrobeRecommendationController'); +const { getStats } = require('../controllers/wardrobe/wardrobeStatsController'); +const { getPublicWardrobes, getPublicWardrobeById } = require('../controllers/wardrobe/wardrobePublicController'); + + + +const router = express.Router(); + +// @desc Get user's wardrobe +// @route GET /api/wardrobe +// @access Private +// router.get('/', protect, async (req, res, next) => { +// try { +// let wardrobe = await Wardrobe.findByUserId(req.user.id); + +// if (!wardrobe) { +// // Create wardrobe if it doesn't exist +// wardrobe = new Wardrobe({ +// userId: req.user.id, +// name: 'My Wardrobe', +// }); +// await wardrobe.save(); +// } + +// res.json({ +// success: true, +// data: { wardrobe }, +// }); +// } catch (error) { +// next(error); +// } +// }); + +router.get('/', protect, getWardrobe); + +// @desc Update wardrobe details +// @route PUT /api/wardrobe +// @access Private +// router.put('/', protect, async (req, res, next) => { +// try { +// const { name, description, isPublic, shareSettings } = req.body; + +// let wardrobe = await Wardrobe.findByUserId(req.user.id); + +// if (!wardrobe) { +// wardrobe = new Wardrobe({ +// userId: req.user.id, +// name: name || 'My Wardrobe', +// description, +// isPublic: isPublic || false, +// shareSettings: shareSettings || { allowViewing: false, allowRecommendations: false }, +// }); +// } else { +// wardrobe.name = name || wardrobe.name; +// wardrobe.description = description; +// wardrobe.isPublic = isPublic !== undefined ? isPublic : wardrobe.isPublic; +// wardrobe.shareSettings = shareSettings || wardrobe.shareSettings; +// } + +// await wardrobe.save(); + +// res.json({ +// success: true, +// message: 'Wardrobe updated successfully', +// data: { wardrobe }, +// }); +// } catch (error) { +// next(error); +// } +// }); + +router.put('/', protect, updateWardrobe); + +// @desc Add item to wardrobe +// @route POST /api/wardrobe/items +// @access Private +// router.post('/items', protect, async (req, res, next) => { +// try { +// const { +// name, +// description, +// category, +// subcategory, +// brand, +// color, +// size, +// material, +// condition, +// images, +// userTags, +// } = req.body; + +// let wardrobe = await Wardrobe.findByUserId(req.user.id); + +// if (!wardrobe) { +// wardrobe = new Wardrobe({ +// userId: req.user.id, +// name: 'My Wardrobe', +// }); +// } + +// const itemData = { +// name, +// description, +// category, +// subcategory, +// brand, +// color, +// size, +// material, +// condition, +// images: images || [], +// userTags: userTags || [], +// }; + +// await wardrobe.addItem(itemData); + +// res.status(201).json({ +// success: true, +// message: 'Item added to wardrobe successfully', +// data: { wardrobe }, +// }); +// } catch (error) { +// next(error); +// } +// }); + +router.post('/items', protect, addItem); + +// @desc Update wardrobe item +// @route PUT /api/wardrobe/items/:itemId +// @access Private +// router.put('/items/:itemId', protect, async (req, res, next) => { +// try { +// const { itemId } = req.params; +// const updateData = req.body; + +// const wardrobe = await Wardrobe.findByUserId(req.user.id); + +// if (!wardrobe) { +// return res.status(404).json({ +// success: false, +// message: 'Wardrobe not found', +// }); +// } + +// await wardrobe.updateItem(itemId, updateData); + +// res.json({ +// success: true, +// message: 'Item updated successfully', +// data: { wardrobe }, +// }); +// } catch (error) { +// next(error); +// } +// }); +router.put('/items/:itemId', protect, updateItem); + +// @desc Remove item from wardrobe +// @route DELETE /api/wardrobe/items/:itemId +// @access Private +// router.delete('/items/:itemId', protect, async (req, res, next) => { +// try { +// const { itemId } = req.params; + +// const wardrobe = await Wardrobe.findByUserId(req.user.id); + +// if (!wardrobe) { +// return res.status(404).json({ +// success: false, +// message: 'Wardrobe not found', +// }); +// } + +// await wardrobe.removeItem(itemId); + +// res.json({ +// success: true, +// message: 'Item removed from wardrobe successfully', +// data: { wardrobe }, +// }); +// } catch (error) { +// next(error); +// } +// }); + +router.delete('/items/:itemId', protect, removeItem); + +// @desc Get items by category +// @route GET /api/wardrobe/items/category/:category +// @access Private +// router.get('/items/category/:category', protect, async (req, res, next) => { +// try { +// const { category } = req.params; + +// const wardrobe = await Wardrobe.findByUserId(req.user.id); + +// if (!wardrobe) { +// return res.status(404).json({ +// success: false, +// message: 'Wardrobe not found', +// }); +// } + +// const items = wardrobe.getItemsByCategory(category); + +// res.json({ +// success: true, +// data: { items }, +// }); +// } catch (error) { +// next(error); +// } +// }); + +router.get('/items/category/:category', protect, getItemsByCategory); + +// @desc Search wardrobe items +// @route GET /api/wardrobe/items/search +// @access Private +// router.get('/items/search', protect, async (req, res, next) => { +// try { +// const { query, tags, category } = req.query; + +// const wardrobe = await Wardrobe.findByUserId(req.user.id); + +// if (!wardrobe) { +// return res.status(404).json({ +// success: false, +// message: 'Wardrobe not found', +// }); +// } + +// let items = wardrobe.items.filter(item => item.isActive); + +// // Filter by category +// if (category) { +// items = items.filter(item => item.category === category); +// } + +// // Filter by tags +// if (tags) { +// const tagArray = tags.split(','); +// items = items.filter(item => +// tagArray.some(tag => +// item.aiTags.includes(tag) || item.userTags.includes(tag) +// ) +// ); +// } + +// // Search by query +// if (query) { +// const searchTerm = query.toLowerCase(); +// items = items.filter(item => +// item.name.toLowerCase().includes(searchTerm) || +// item.brand?.toLowerCase().includes(searchTerm) || +// item.description?.toLowerCase().includes(searchTerm) +// ); +// } + +// res.json({ +// success: true, +// data: { items }, +// }); +// } catch (error) { +// next(error); +// } +// }); +router.get('/items/search', protect, searchItems); + +// @desc Generate outfit recommendations +// @route GET /api/wardrobe/recommendations +// @access Private +// router.get('/recommendations', protect, async (req, res, next) => { +// try { +// const wardrobe = await Wardrobe.findByUserId(req.user.id); + +// if (!wardrobe) { +// return res.status(404).json({ +// success: false, +// message: 'Wardrobe not found', +// }); +// } + +// const recommendations = wardrobe.generateOutfitRecommendations(); + +// res.json({ +// success: true, +// data: { recommendations }, +// }); +// } catch (error) { +// next(error); +// } +// }); +router.get('/recommendations', protect, getRecommendations); + +// @desc Get wardrobe statistics +// @route GET /api/wardrobe/stats +// @access Private +// router.get('/stats', protect, async (req, res, next) => { +// try { +// const wardrobe = await Wardrobe.findByUserId(req.user.id); + +// if (!wardrobe) { +// return res.status(404).json({ +// success: false, +// message: 'Wardrobe not found', +// }); +// } + +// const stats = { +// totalItems: wardrobe.totalItems, +// categoryCounts: wardrobe.categoryCounts, +// recentItems: wardrobe.items +// .sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt)) +// .slice(0, 5), +// mostWornItems: wardrobe.items +// .sort((a, b) => (b.purchaseCount || 0) - (a.purchaseCount || 0)) +// .slice(0, 5), +// }; + +// res.json({ +// success: true, +// data: { stats }, +// }); +// } catch (error) { +// next(error); +// } +// }); +router.get('/stats', protect, getStats); + +// @desc Get public wardrobes +// @route GET /api/wardrobe/public +// @access Public +// router.get('/public', async (req, res, next) => { +// try { +// const { page = 1, limit = 20 } = req.query; +// const skip = (page - 1) * limit; + +// const wardrobes = await Wardrobe.findPublicWardrobes( +// parseInt(limit), +// parseInt(skip) +// ); + +// res.json({ +// success: true, +// data: { wardrobes }, +// }); +// } catch (error) { +// next(error); +// } +// }); + +router.get('/public', getPublicWardrobes); + +// @desc Get public wardrobe by ID +// @route GET /api/wardrobe/public/:id +// @access Public +// router.get('/public/:id', async (req, res, next) => { +// try { +// const wardrobe = await Wardrobe.findOne({ +// _id: req.params.id, +// isPublic: true, +// }); + +// if (!wardrobe) { +// return res.status(404).json({ +// success: false, +// message: 'Wardrobe not found', +// }); +// } + +// res.json({ +// success: true, +// data: { wardrobe }, +// }); +// } catch (error) { +// next(error); +// } +// }); + +router.get('/public/:id', getPublicWardrobeById); + +module.exports = router; diff --git a/src/scripts/addStockFieldToProducts.js b/src/scripts/addStockFieldToProducts.js new file mode 100644 index 0000000..8ad2010 --- /dev/null +++ b/src/scripts/addStockFieldToProducts.js @@ -0,0 +1,149 @@ +// scripts/addStockFieldToProducts.js - MIGRATION SCRIPT + +const mongoose = require('mongoose'); +require('dotenv').config(); + +/** + * Add stock field to all existing products in MongoDB + */ +async function addStockFieldToProducts() { + try { + console.log('๐Ÿ”„ Starting stock field migration...\n'); + + // Connect to MongoDB + await mongoose.connect(process.env.MONGODB_URI || process.env.MONGO_URI); + console.log('โœ… Connected to MongoDB\n'); + + const db = mongoose.connection.db; + const productsCollection = db.collection('products'); + + // Get all products + const allProducts = await productsCollection.find({}).toArray(); + console.log(`๐Ÿ“ฆ Found ${allProducts.length} total products\n`); + + // Separate products by variant status + const withVariants = allProducts.filter(p => p.hasVariants && p.variants?.length > 0); + const withoutVariants = allProducts.filter(p => !p.hasVariants || !p.variants?.length); + + console.log(` ${withVariants.length} products with variants`); + console.log(` ${withoutVariants.length} products without variants\n`); + + let updated = 0; + let skipped = 0; + + // Update products WITHOUT variants + console.log('๐Ÿ”„ Adding stock field to simple products...\n'); + + for (const product of withoutVariants) { + // Skip if stock field already exists + if (product.stock !== undefined && product.stock !== null) { + console.log(` โญ๏ธ ${product.name}: Already has stock (${product.stock})`); + skipped++; + continue; + } + + // Add stock field (default 50 for existing products) + const DEFAULT_STOCK = 50; + + await productsCollection.updateOne( + { _id: product._id }, + { + $set: { + stock: DEFAULT_STOCK, + trackInventory: true, + updatedAt: new Date() + } + } + ); + + console.log(` โœ… ${product.name}: stock = ${DEFAULT_STOCK}`); + updated++; + } + + // For products WITH variants, set stock to 0 (they use variant inventory) + console.log(`\n๐Ÿ”„ Setting stock=0 for variant products...\n`); + + for (const product of withVariants) { + await productsCollection.updateOne( + { _id: product._id }, + { + $set: { + stock: 0, + trackInventory: false, + updatedAt: new Date() + } + } + ); + + console.log(` โœ… ${product.name}: stock = 0 (uses variants)`); + updated++; + } + + // Verification + console.log(`\n๐Ÿ“Š Verifying migration...\n`); + + const verification = await productsCollection.aggregate([ + { + $group: { + _id: null, + totalProducts: { $sum: 1 }, + withStock: { + $sum: { + $cond: [{ $ifNull: ['$stock', false] }, 1, 0] + } + }, + avgStock: { $avg: '$stock' }, + totalStock: { $sum: '$stock' } + } + } + ]).toArray(); + + if (verification.length > 0) { + const stats = verification[0]; + console.log(` Total Products: ${stats.totalProducts}`); + console.log(` With Stock Field: ${stats.withStock}`); + console.log(` Average Stock: ${Math.round(stats.avgStock || 0)}`); + console.log(` Total Stock: ${stats.totalStock || 0}`); + } + + console.log(`\nโœ… Migration completed!`); + console.log(` Updated: ${updated} products`); + console.log(` Skipped: ${skipped} products (already had stock)\n`); + + // Stock distribution + const stockDistribution = await productsCollection.aggregate([ + { + $bucket: { + groupBy: '$stock', + boundaries: [0, 1, 6, 11, 51], + default: 'Other', + output: { + count: { $sum: 1 }, + products: { $push: '$name' } + } + } + } + ]).toArray(); + + console.log('๐Ÿ“ˆ Stock Distribution:'); + stockDistribution.forEach(bucket => { + const range = bucket._id === 'Other' ? 'Other' : + bucket._id === 0 ? '0 (Out of Stock)' : + bucket._id === 1 ? '1-5 (Critical)' : + bucket._id === 6 ? '6-10 (Low)' : + '11-50 (In Stock)'; + console.log(` ${range}: ${bucket.count} products`); + }); + + await mongoose.connection.close(); + console.log('\nโœ… Database connection closed'); + process.exit(0); + + } catch (error) { + console.error('\nโŒ Migration error:', error); + process.exit(1); + } +} + +// Run migration +addStockFieldToProducts(); \ No newline at end of file diff --git a/src/scripts/migrateCategoryIds.js b/src/scripts/migrateCategoryIds.js new file mode 100644 index 0000000..ebe0d64 --- /dev/null +++ b/src/scripts/migrateCategoryIds.js @@ -0,0 +1,48 @@ +const mongoose = require('mongoose'); +const Product = require('../models/mongodb/Product'); +require('dotenv').config(); + +const migrateCategoryIds = async () => { + try { + // Connect to MongoDB + await mongoose.connect(process.env.MONGODB_URI || 'your_mongodb_connection_string'); + console.log('Connected to MongoDB'); + + // Map old category IDs to new ones + const categoryMapping = { + 'Clothing': 'cmiu33j770005141mz54xgsqe', // Fashion category + '68c123e87e7f9a9b8b123456': 'cmiu34dfg0009141mn8r1dujd', // Men Clothing + '68c123e87e7f9a9b8b123457': 'cmiu355i2000b141m2o7aqlb2', // Women Clothing + '68c123e87e7f9a9b8b123458': 'cmiu3a7je000l141mwx9boup4', // Western Wear + '68c123e87e7f9a9b8b123459': 'cmiu39ncw000j141mxizow1p2', // Lehengas + '68c123e87e7f9a9b8b123460': 'cmiu384il000f141m6obcit4u', // Sarees + '68c123e87e7f9a9b8b123461': 'cmiu39ncw000j141mxizow1p2', // Lehengas + '68c123e87e7f9a9b8b123462': 'cmiu3cuwy000t141mkt4weuy5', // Ethnic Wear + '68c123e87e7f9a9b8b123463': 'cmiu3a7je000l141mwx9boup4', // Western Wear + 'cmh4io0fv0001145t4057y8dw': 'cmiu34dfg0009141mn8r1dujd', // Men Clothing + }; + + let totalUpdated = 0; + + for (const [oldId, newId] of Object.entries(categoryMapping)) { + const result = await Product.updateMany( + { category: oldId }, + { $set: { category: newId } } + ); + totalUpdated += result.modifiedCount; + console.log(`โœ… Updated ${result.modifiedCount} products from ${oldId} to ${newId}`); + } + + console.log(`\n๐ŸŽ‰ Migration complete! Total products updated: ${totalUpdated}`); + + // Close connection + await mongoose.connection.close(); + console.log('MongoDB connection closed'); + process.exit(0); + } catch (error) { + console.error('โŒ Migration failed:', error); + process.exit(1); + } +}; + +migrateCategoryIds(); \ No newline at end of file diff --git a/src/server.js b/src/server.js new file mode 100644 index 0000000..e1c7078 --- /dev/null +++ b/src/server.js @@ -0,0 +1,149 @@ +require('dotenv').config(); +// import uploadRoutes from "./routes/upload.routes"; +const uploadRoutes = require('./routes/upload.routes'); + +const express = require('express'); +const cors = require('cors'); +const helmet = require('helmet'); +const morgan = require('morgan'); +const cookieParser = require('cookie-parser'); + +const { + initializeDatabases, + closeDatabaseConnections, +} = require('./config/database'); +const { errorHandler, notFound } = require('./middleware/errorHandler'); + +const app = express(); +const PORT = process.env.PORT || 3000; + +// Security middleware +app.use( + helmet({ + contentSecurityPolicy: false, // Disable for API + crossOriginEmbedderPolicy: false, + }) +); + +const allowedOrigins = process.env.CORS_ORIGIN + ? process.env.CORS_ORIGIN.split(',').map(origin => origin.trim()) + : []; + +const corsOptions = { + origin: function (origin, callback) { + // Allow requests with no origin (like Postman, mobile apps) + if (!origin) return callback(null, true); + + if (allowedOrigins.includes(origin)) { + callback(null, true); + } else { + callback(new Error('Not allowed by CORS')); + } + }, + credentials: true, + methods: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'OPTIONS'], + allowedHeaders: ['Content-Type', 'Authorization', 'X-Requested-With'], +}; + + +app.use(cors(corsOptions)); + +// Body parsing middleware +app.use(express.json({ limit: '10mb' })); +app.use(express.urlencoded({ extended: true, limit: '10mb' })); +app.use(cookieParser()); + +// Logging middleware +if (process.env.NODE_ENV !== 'test') { + app.use(morgan('combined')); +} + +// Health check endpoint +app.get('/health', async (req, res) => { + try { + res.status(200).json({ + status: 'SUCCESS', + message: 'Vaishnavi Creation API is running successfully ๐Ÿš€', + service: 'vaishnavi-backend', + version: '1.0.0', + environment: process.env.NODE_ENV, + timestamp: new Date().toISOString(), + uptime_seconds: process.uptime(), + memory_usage: process.memoryUsage().rss, // RAM usage + }); + } catch (error) { + res.status(500).json({ + status: 'ERROR', + message: 'Health check failed', + error: error.message, + }); + } +}); + +// API Routes +app.use('/api/auth', require('./routes/auth')); +app.use('/api/users', require('./routes/users')); +app.use('/api/products', require('./routes/products')); +app.use('/api/orders', require('./routes/orders')); +app.use('/api/wardrobe', require('./routes/wardrobe')); +app.use('/api/delivery', require('./routes/deliveryRoutes')); +app.use('/api/coupons', require('./routes/couponRoutes')); +app.use('/api/admin', require('./routes/admin')); + +app.use('/api/admin/reports', require('./routes/reports')); +app.use('/api/payments', require('./routes/paymentRoutes')); +// Upload route +app.use('/api', uploadRoutes); + +// Root endpoint +app.get('/', (req, res) => { + res.json({ + message: 'Vaishnavi Creation API', + version: '1.0.0', + documentation: '/api/docs', + health: '/health', + }); +}); + +// Error handling middleware (must be last) +app.use(notFound); +app.use(errorHandler); + +// Graceful shutdown +process.on('SIGTERM', async () => { + console.log('SIGTERM received, shutting down gracefully'); + await closeDatabaseConnections(); + process.exit(0); +}); + +process.on('SIGINT', async () => { + console.log('SIGINT received, shutting down gracefully'); + await closeDatabaseConnections(); + process.exit(0); +}); + +// Start server +const startServer = async () => { + try { + // Initialize database connections + await initializeDatabases(); + + // Start the server + app.listen(PORT, () => { + console.log(`๐Ÿš€ Server running on port ${PORT}`); + console.log(`๐Ÿ“š API Documentation: http://localhost:${PORT}/api/docs`); + console.log(`๐Ÿฅ Health Check: http://localhost:${PORT}/health`); + console.log(`๐ŸŒ Environment: ${process.env.NODE_ENV}`); + }); + } catch (error) { + console.error('โŒ Failed to start server:', error); + process.exit(1); + } +}; + +// Only start server if this file is run directly +if (require.main === module) { + startServer(); +} + +module.exports = app; diff --git a/src/services/authService.js b/src/services/authService.js new file mode 100644 index 0000000..42d8b65 --- /dev/null +++ b/src/services/authService.js @@ -0,0 +1,348 @@ +const bcrypt = require('bcrypt'); +const jwt = require('jsonwebtoken'); +const { prisma } = require('../config/database'); +const sendEmail = require('../utils/mailer'); + +class AuthService { + // Generate JWT token + generateToken(payload) { + return jwt.sign(payload, process.env.JWT_SECRET, { + expiresIn: process.env.JWT_EXPIRES_IN || '7d', + }); + } + + // Generate refresh token + generateRefreshToken(payload) { + return jwt.sign(payload, process.env.JWT_REFRESH_SECRET, { + expiresIn: process.env.JWT_REFRESH_EXPIRES_IN || '30d', + }); + } + + // Hash password + async hashPassword(password) { + const saltRounds = 12; + return await bcrypt.hash(password, saltRounds); + } + + // Compare password + async comparePassword(password, hashedPassword) { + return await bcrypt.compare(password, hashedPassword); + } + + // Register new user + async register(userData) { + const { email, password, firstName, lastName, username, phone } = userData; + + // Check if user already exists + const existingUser = await prisma.user.findFirst({ + where: { + OR: [{ email }, ...(username ? [{ username }] : [])], + }, + }); + + if (existingUser) { + throw new Error('User with this email or username already exists'); + } + + // Hash password + const passwordHash = await this.hashPassword(password); + + // Create user + const user = await prisma.user.create({ + data: { + email, + passwordHash, + firstName, + lastName, + username, + phone, + // role: 'CUSTOMER', + role: userData.role || 'CUSTOMER', + isVerified: false, + isActive: true, + }, + select: { + id: true, + email: true, + username: true, + firstName: true, + lastName: true, + role: true, + isVerified: true, + createdAt: true, + }, + }); + + // Generate tokens + const token = this.generateToken({ id: user.id }); + const refreshToken = this.generateRefreshToken({ id: user.id }); + + return { + user, + token, + refreshToken, + }; + } + + // Login user + async login(email, password) { + // Find user by email + const user = await prisma.user.findUnique({ + where: { email }, + }); + + if (!user) { + throw new Error('Invalid credentials'); + } + + // Check if user is active + if (!user.isActive) { + throw new Error('Account is deactivated'); + } + + // Verify password + const isPasswordValid = await this.comparePassword( + password, + user.passwordHash + ); + if (!isPasswordValid) { + throw new Error('Invalid credentials'); + } + + // Update last login + await prisma.user.update({ + where: { id: user.id }, + data: { lastLoginAt: new Date() }, + }); + + // Generate tokens + const token = this.generateToken({ id: user.id }); + const refreshToken = this.generateRefreshToken({ id: user.id }); + + // Return user data (without password) + const userData = { + id: user.id, + email: user.email, + username: user.username, + firstName: user.firstName, + lastName: user.lastName, + role: user.role, + isVerified: user.isVerified, + avatar: user.avatar, + lastLoginAt: user.lastLoginAt, + }; + + return { + user: userData, + token, + refreshToken, + }; + } + + // Refresh token + async refreshToken(refreshToken) { + try { + const decoded = jwt.verify(refreshToken, process.env.JWT_REFRESH_SECRET); + + // Find user + const user = await prisma.user.findUnique({ + where: { id: decoded.id }, + select: { + id: true, + email: true, + username: true, + firstName: true, + lastName: true, + role: true, + isVerified: true, + isActive: true, + }, + }); + + if (!user || !user.isActive) { + throw new Error('Invalid refresh token'); + } + + // Generate new tokens + const newToken = this.generateToken({ id: user.id }); + const newRefreshToken = this.generateRefreshToken({ id: user.id }); + + return { + token: newToken, + refreshToken: newRefreshToken, + }; + } catch (error) { + throw new Error('Invalid refresh token'); + } + } + + // Change password + async changePassword(userId, currentPassword, newPassword) { + // Find user + const user = await prisma.user.findUnique({ + where: { id: userId }, + }); + + if (!user) { + throw new Error('User not found'); + } + + // Verify current password + const isCurrentPasswordValid = await this.comparePassword( + currentPassword, + user.passwordHash + ); + if (!isCurrentPasswordValid) { + throw new Error('Current password is incorrect'); + } + + // Hash new password + const newPasswordHash = await this.hashPassword(newPassword); + + // Update password + await prisma.user.update({ + where: { id: userId }, + data: { passwordHash: newPasswordHash }, + }); + + return { message: 'Password changed successfully' }; + } + + // Reset password request + async requestPasswordReset(email) { + const user = await prisma.user.findUnique({ + where: { email }, + }); + + if (!user) { + // Don't reveal if user exists or not + return { message: 'If the email exists, a reset link has been sent' }; + } + + // Generate reset token + const resetToken = jwt.sign( + { id: user.id, type: 'password_reset' }, + process.env.JWT_SECRET, + { expiresIn: '1h' } + ); + + // Use URL from env + const resetUrl = `${process.env.FRONTEND_URL}/reset-password?token=${resetToken}`; + + // In a real application, you would: + // 1. Store the reset token in database with expiry + // 2. Send email with reset link + // 3. Use a proper email service + // Send email + await sendEmail(user.email, 'Reset Your Password', 'reset-password', { + firstName: user.firstName || '', + resetUrl, + }); + + // await sendEmail(user.email, 'Reset Your Password', html); + + console.log(`Password reset token for ${email}: ${resetToken}`); + + return { message: 'If the email exists, a reset link has been sent' }; + } + + // Reset password with token + async resetPassword(token, newPassword) { + try { + const decoded = jwt.verify(token, process.env.JWT_SECRET); + + if (decoded.type !== 'password_reset') { + throw new Error('Invalid reset token'); + } + + // Find user + const user = await prisma.user.findUnique({ + where: { id: decoded.id }, + }); + + if (!user) { + throw new Error('User not found'); + } + + // Hash new password + const passwordHash = await this.hashPassword(newPassword); + + // Update password + await prisma.user.update({ + where: { id: user.id }, + data: { passwordHash }, + }); + + return { message: 'Password reset successfully' }; + } catch (error) { + throw new Error('Invalid or expired reset token'); + } + } + + // Verify email + async verifyEmail(token) { + try { + const decoded = jwt.verify(token, process.env.JWT_SECRET); + + if (decoded.type !== 'email_verification') { + throw new Error('Invalid verification token'); + } + + // Update user verification status + await prisma.user.update({ + where: { id: decoded.id }, + data: { isVerified: true }, + }); + + return { message: 'Email verified successfully' }; + } catch (error) { + throw new Error('Invalid or expired verification token'); + } + } + + // Send verification email + async sendVerificationEmail(userId) { + const user = await prisma.user.findUnique({ + where: { id: userId }, + }); + + if (!user) { + throw new Error('User not found'); + } + + if (user.isVerified) { + throw new Error('Email already verified'); + } + + // Generate verification token + const verificationToken = jwt.sign( + { id: user.id, type: 'email_verification' }, + process.env.JWT_SECRET, + { expiresIn: '24h' } + ); + const verificationUrl = `${process.env.FRONTEND_URL}/verify-email?token=${verificationToken}`; + + await sendEmail(user.email, 'Verify Your Email', 'verify-email', { + firstName: user.firstName || '', + verificationUrl, + }); + + // await sendEmail(user.email, 'Verify Your Email', html); + // In a real application, you would send an email here + console.log(`Verification token for ${user.email}: ${verificationToken}`); + + return { message: 'Verification email sent' }; + } + + // Logout (invalidate token) + async logout(userId) { + // In a real application, you might want to: + // 1. Add token to a blacklist + // 2. Store invalidated tokens in Redis + // 3. Implement proper session management + + return { message: 'Logged out successfully' }; + } +} + +module.exports = new AuthService(); diff --git a/src/services/deliveryEstimationService.js b/src/services/deliveryEstimationService.js new file mode 100644 index 0000000..0a5c9d5 --- /dev/null +++ b/src/services/deliveryEstimationService.js @@ -0,0 +1,266 @@ +// services/deliveryEstimationService.js + +/** + * Delivery Estimation Service + * Calculates estimated delivery dates based on: + * - User's pincode/location + * - Order placement time + * - Shipping method + * - Product availability + */ + +// Pincode to delivery days mapping (Indian postal system) +const PINCODE_ZONES = { + // Metro Cities (1-2 days) + METRO: { + pincodes: [ + // Delhi NCR + /^110\d{3}$/, /^121\d{3}$/, /^122\d{3}$/, /^201\d{3}$/, + // Mumbai + /^400\d{3}$/, /^401\d{3}$/, + // Bangalore + /^560\d{3}$/, + // Chennai + /^600\d{3}$/, + // Hyderabad + /^500\d{3}$/, + // Kolkata + /^700\d{3}$/, + // Pune + /^411\d{3}$/, + // Ahmedabad + /^380\d{3}$/, + ], + deliveryDays: { min: 1, max: 2 }, + name: 'Metro Cities', + }, + + // Tier 1 Cities (2-4 days) + TIER_1: { + pincodes: [ + /^141\d{3}$/, // Ludhiana + /^160\d{3}$/, // Chandigarh + /^226\d{3}$/, // Lucknow + /^302\d{3}$/, // Jaipur + /^390\d{3}$/, // Vadodara + /^395\d{3}$/, // Surat + /^422\d{3}$/, // Nashik + /^440\d{3}$/, // Nagpur + /^462\d{3}$/, // Bhopal + /^482\d{3}$/, // Indore + /^492\d{3}$/, // Raipur + /^520\d{3}$/, // Vijayawada + /^530\d{3}$/, // Visakhapatnam + /^570\d{3}$/, // Mysore + /^641\d{3}$/, // Coimbatore + /^682\d{3}$/, // Kochi + /^695\d{3}$/, // Trivandrum + /^751\d{3}$/, // Bhubaneswar + /^781\d{3}$/, // Guwahati + /^800\d{3}$/, // Patna + ], + deliveryDays: { min: 2, max: 4 }, + name: 'Tier 1 Cities', + }, + + // Tier 2 Cities (3-5 days) + TIER_2: { + pincodes: [ + /^1[0-9]{5}$/, // North India (excluding metros) + /^2[0-9]{5}$/, // West India (excluding metros) + /^3[0-9]{5}$/, // Gujarat/Rajasthan + /^4[0-9]{5}$/, // Maharashtra (excluding metros) + /^5[0-9]{5}$/, // South India (excluding metros) + /^6[0-9]{5}$/, // Tamil Nadu/Kerala (excluding metros) + /^7[0-9]{5}$/, // East India + /^8[0-9]{5}$/, // Bihar/Jharkhand + ], + deliveryDays: { min: 3, max: 5 }, + name: 'Tier 2 Cities', + }, + + // Remote Areas (5-7 days) + REMOTE: { + pincodes: [ + /^17[0-9]{4}$/, // Himachal Pradesh + /^18[0-9]{4}$/, // J&K + /^19[0-9]{4}$/, // J&K + /^73[0-9]{4}$/, // Arunachal Pradesh + /^79[0-9]{4}$/, // Assam/Meghalaya + /^82[0-9]{4}$/, // Jharkhand (remote) + /^84[0-9]{4}$/, // Bihar (remote) + /^85[0-9]{4}$/, // Orissa (remote) + ], + deliveryDays: { min: 5, max: 7 }, + name: 'Remote Areas', + }, + + // Default (4-6 days) + DEFAULT: { + deliveryDays: { min: 4, max: 6 }, + name: 'Standard Delivery', + }, +}; + +/** + * Get delivery zone based on pincode + */ +function getDeliveryZone(pincode) { + const cleanPincode = pincode.replace(/\s+/g, ''); + + // Check Metro + if (PINCODE_ZONES.METRO.pincodes.some(pattern => pattern.test(cleanPincode))) { + return PINCODE_ZONES.METRO; + } + + // Check Tier 1 + if (PINCODE_ZONES.TIER_1.pincodes.some(pattern => pattern.test(cleanPincode))) { + return PINCODE_ZONES.TIER_1; + } + + // Check Tier 2 + if (PINCODE_ZONES.TIER_2.pincodes.some(pattern => pattern.test(cleanPincode))) { + return PINCODE_ZONES.TIER_2; + } + + // Check Remote + if (PINCODE_ZONES.REMOTE.pincodes.some(pattern => pattern.test(cleanPincode))) { + return PINCODE_ZONES.REMOTE; + } + + // Default + return PINCODE_ZONES.DEFAULT; +} + +/** + * Calculate estimated delivery date + */ +function calculateDeliveryDate(pincode, orderDate = new Date(), shippingMethod = 'STANDARD') { + const zone = getDeliveryZone(pincode); + let { min, max } = zone.deliveryDays; + + // Adjust for shipping method + if (shippingMethod === 'EXPRESS') { + min = Math.max(1, min - 1); + max = Math.max(2, max - 1); + } + + // Calculate dates + const minDate = addBusinessDays(orderDate, min); + const maxDate = addBusinessDays(orderDate, max); + + return { + zone: zone.name, + estimatedDays: { min, max }, + estimatedDelivery: { + min: minDate, + max: maxDate, + formatted: formatDateRange(minDate, maxDate), + }, + shippingMethod, + }; +} + +/** + * Add business days (excluding Sundays) + */ +function addBusinessDays(date, days) { + let currentDate = new Date(date); + let addedDays = 0; + + while (addedDays < days) { + currentDate.setDate(currentDate.getDate() + 1); + + // Skip Sundays (0 = Sunday) + if (currentDate.getDay() !== 0) { + addedDays++; + } + } + + return currentDate; +} + +/** + * Format date range for display + */ +function formatDateRange(minDate, maxDate) { + const options = { + weekday: 'short', + month: 'short', + day: 'numeric' + }; + + const minFormatted = minDate.toLocaleDateString('en-IN', options); + const maxFormatted = maxDate.toLocaleDateString('en-IN', options); + + // If same month + if (minDate.getMonth() === maxDate.getMonth()) { + return `${minDate.getDate()}-${maxDate.getDate()} ${minDate.toLocaleDateString('en-IN', { month: 'short' })}`; + } + + return `${minFormatted} - ${maxFormatted}`; +} + +/** + * Get delivery estimation for checkout + */ +async function getDeliveryEstimation(pincode, shippingMethod = 'STANDARD') { + try { + const estimation = calculateDeliveryDate(pincode, new Date(), shippingMethod); + + return { + success: true, + data: { + ...estimation, + message: `Estimated delivery by ${estimation.estimatedDelivery.formatted}`, + canDeliver: true, + }, + }; + } catch (error) { + console.error('Delivery estimation error:', error); + return { + success: false, + message: 'Unable to calculate delivery time', + }; + } +} + +/** + * Check if pincode is serviceable + */ +function isServiceable(pincode) { + const cleanPincode = pincode.replace(/\s+/g, ''); + + // Basic validation: Indian pincodes are 6 digits + if (!/^\d{6}$/.test(cleanPincode)) { + return false; + } + + // All Indian pincodes are serviceable + // You can add specific non-serviceable pincodes here if needed + const nonServiceablePincodes = [ + // Add any non-serviceable pincodes + ]; + + return !nonServiceablePincodes.includes(cleanPincode); +} + +/** + * Get delivery speed label + */ +function getDeliverySpeedLabel(days) { + if (days <= 2) return 'Express Delivery'; + if (days <= 4) return 'Fast Delivery'; + if (days <= 6) return 'Standard Delivery'; + return 'Extended Delivery'; +} + +module.exports = { + calculateDeliveryDate, + getDeliveryEstimation, + getDeliveryZone, + isServiceable, + getDeliverySpeedLabel, + addBusinessDays, + PINCODE_ZONES, +}; \ No newline at end of file diff --git a/src/services/inventoryService.js b/src/services/inventoryService.js new file mode 100644 index 0000000..fc3afd8 --- /dev/null +++ b/src/services/inventoryService.js @@ -0,0 +1,264 @@ +// services/inventoryService.js - COMPLETE INVENTORY SYSTEM +const { prisma } = require('../config/database'); +const Product = require('../models/mongodb/Product'); + +/** + * โœ… Auto-reduce stock when order is DELIVERED + */ +async function reduceStockOnDelivery(orderId) { + try { + const order = await prisma.order.findUnique({ + where: { id: orderId }, + include: { items: true }, + }); + + if (!order || order.status !== 'DELIVERED') return null; + + const results = []; + + for (const item of order.items) { + const product = await Product.findById(item.productId); + if (!product) continue; + + let previousStock, newStock; + + if (product.hasVariants && product.variants?.length > 0) { + // โœ… Reduce from matching variant by SKU + previousStock = product.variants + .filter(v => v.isActive) + .reduce((sum, v) => sum + (v.inventory?.quantity || 0), 0); + + const variantIndex = product.variants.findIndex( + v => v.sku === item.productSku + ); + + if (variantIndex !== -1) { + const currentQty = + product.variants[variantIndex].inventory?.quantity || 0; + product.variants[variantIndex].inventory.quantity = Math.max( + 0, + currentQty - item.quantity + ); + await product.save(); + } + + newStock = product.variants + .filter(v => v.isActive) + .reduce((sum, v) => sum + (v.inventory?.quantity || 0), 0); + } else { + // โœ… Non-variant product + previousStock = product.stock || 0; + newStock = Math.max(0, previousStock - item.quantity); + await Product.findByIdAndUpdate(item.productId, { + stock: newStock, + updatedAt: new Date(), + }); + } + + await prisma.inventoryLog.create({ + data: { + productId: item.productId, + productName: item.productName || product.name, + type: 'SOLD', + quantityChange: -item.quantity, + previousStock, + newStock, + orderId, + notes: `Order ${order.orderNumber} delivered`, + }, + }); + + results.push({ + productId: item.productId, + productName: product.name, + reduced: item.quantity, + previousStock, + newStock, + }); + } + + return results; + } catch (error) { + console.error('โŒ Stock reduction error:', error); + throw error; + } +} + +/** + * โœ… Get low stock products + */ +async function getLowStockProducts(threshold = 10) { + try { + const products = await Product.find({ status: 'active' }).lean(); + + const withStock = products.map(product => { + let totalStock = 0; + + if (product.hasVariants && product.variants?.length > 0) { + totalStock = product.variants + .filter(v => v.isActive) + .reduce((sum, v) => sum + (v.inventory?.quantity || 0), 0); + } else { + totalStock = product.stock || 0; + } + + return { + _id: product._id.toString(), + name: product.name, + slug: product.slug, + stock: totalStock, + basePrice: product.basePrice, + hasVariants: product.hasVariants, + status: + totalStock === 0 + ? 'OUT_OF_STOCK' + : totalStock <= 5 + ? 'CRITICAL' + : 'LOW', + displayImage: getProductImage(product), + }; + }); + + return withStock + .filter(p => p.stock <= threshold) + .sort((a, b) => a.stock - b.stock) + .slice(0, threshold); + } catch (error) { + console.error('Error fetching low stock:', error); + return []; + } +} + +/** + * โœ… Get inventory stats for dashboard + */ +async function getInventoryStats() { + try { + const products = await Product.find({ status: 'active' }).lean(); + + let outOfStock = 0, + criticalStock = 0, + lowStock = 0, + inStock = 0; + + products.forEach(product => { + let stock = 0; + + if (product.hasVariants && product.variants?.length > 0) { + stock = product.variants + .filter(v => v.isActive) + .reduce((sum, v) => sum + (v.inventory?.quantity || 0), 0); + } else { + stock = product.stock || 0; + } + + if (stock === 0) outOfStock++; + else if (stock <= 5) criticalStock++; + else if (stock <= 10) lowStock++; + else inStock++; + }); + + return { + totalProducts: products.length, + outOfStock, + criticalStock, + lowStock, + inStock, + }; + } catch (error) { + console.error('Error fetching inventory stats:', error); + return null; + } +} + +/** + * โœ… Manual stock adjustment (Admin) + */ +async function adjustStock( + productId, + variantSku = null, + quantity, + type, + notes, + adminId +) { + try { + const product = await Product.findById(productId); + if (!product) throw new Error('Product not found'); + + let previousStock, newStock; + + if (product.hasVariants && product.variants?.length > 0 && variantSku) { + // โœ… Adjust specific variant + const variantIndex = product.variants.findIndex( + v => v.sku === variantSku + ); + if (variantIndex === -1) throw new Error('Variant not found'); + + const currentQty = + product.variants[variantIndex].inventory?.quantity || 0; + previousStock = currentQty; + + if (type === 'ADD') newStock = currentQty + quantity; + else if (type === 'REMOVE') newStock = Math.max(0, currentQty - quantity); + else if (type === 'SET') newStock = quantity; + else throw new Error('Invalid type'); + + product.variants[variantIndex].inventory.quantity = newStock; + await product.save(); + } else { + // โœ… Non-variant product + previousStock = product.stock || 0; + + if (type === 'ADD') newStock = previousStock + quantity; + else if (type === 'REMOVE') + newStock = Math.max(0, previousStock - quantity); + else if (type === 'SET') newStock = quantity; + else throw new Error('Invalid type'); + + await Product.findByIdAndUpdate(productId, { + stock: newStock, + updatedAt: new Date(), + }); + } + + await prisma.inventoryLog.create({ + data: { + productId, + productName: product.name, + type: type === 'ADD' ? 'RESTOCK' : 'ADJUSTMENT', + quantityChange: type === 'ADD' ? quantity : -quantity, + previousStock, + newStock, + notes: notes || `Manual ${type} by admin`, + adjustedBy: adminId, + }, + }); + + return { + success: true, + productName: product.name, + previousStock, + newStock, + }; + } catch (error) { + console.error('Error adjusting stock:', error); + throw error; + } +} + +function getProductImage(product) { + return ( + product.images?.gallery?.[0] || + product.images?.primary || + product.variants?.[0]?.images?.[0] || + 'https://via.placeholder.com/300' + ); +} + +module.exports = { + reduceStockOnDelivery, + getLowStockProducts, + getInventoryStats, + adjustStock, +}; diff --git a/src/services/otpStore.js b/src/services/otpStore.js new file mode 100644 index 0000000..e5cbbe0 --- /dev/null +++ b/src/services/otpStore.js @@ -0,0 +1,59 @@ +// Simple in-memory OTP store +// Replace with Redis in production: await redis.setex(key, 300, otp) +const otpStore = new Map(); + +const OTP_EXPIRY_MS = 5 * 60 * 1000; // 5 minutes + +// โœ… TEST OTP โ€” works in any environment when used +const TEST_OTP = '123456'; +const TEST_PHONES = ['9999999999']; // add any test phone numbers here + + + +function saveOTP(phone, otp) { + otpStore.set(phone, { + otp, + expiresAt: Date.now() + OTP_EXPIRY_MS, + attempts: 0, + }); +} + +function verifyOTP(phone, inputOtp) { + // โœ… Master bypass OTP โ€” always works in development + if (process.env.NODE_ENV === 'development' && inputOtp === TEST_OTP) { + console.log(`๐Ÿ”“ Test OTP used for ${phone}`); + return { valid: true }; + } + + // โœ… Test phone numbers skip WhatsApp entirely + if (TEST_PHONES.includes(phone) && inputOtp === TEST_OTP) { + return { valid: true }; + } + + // Normal OTP verification below... + const record = otpStore.get(phone); + if (!record) return { valid: false, reason: 'OTP not found or expired' }; + if (Date.now() > record.expiresAt) { + otpStore.delete(phone); + return { valid: false, reason: 'OTP expired' }; + } + + record.attempts++; + if (record.attempts > 3) { + otpStore.delete(phone); + return { valid: false, reason: 'Too many attempts' }; + } + + if (record.otp !== inputOtp) { + return { valid: false, reason: 'Invalid OTP' }; + } + + otpStore.delete(phone); + return { valid: true }; +} + +function clearOTP(phone) { + otpStore.delete(phone); +} + +module.exports = { saveOTP, verifyOTP, clearOTP }; \ No newline at end of file diff --git a/src/services/s3Upload.service.js b/src/services/s3Upload.service.js new file mode 100644 index 0000000..3fe57a9 --- /dev/null +++ b/src/services/s3Upload.service.js @@ -0,0 +1,25 @@ +// services/s3Upload.service.js +import { PutObjectCommand } from "@aws-sdk/client-s3"; +import s3 from "../config/s3.js"; +import fs from "fs"; + +export const uploadToS3 = async (file) => { + const fileStream = fs.createReadStream(file.path); + + const key = `products/${Date.now()}-${file.originalname}`; + + await s3.send( + new PutObjectCommand({ + Bucket: process.env.AWS_S3_BUCKET, + Key: key, + Body: fileStream, + ContentType: file.mimetype, + ACL: "public-read", + }) + ); + + // optional cleanup + fs.unlinkSync(file.path); + + return `https://s3.sahasrarameta.tech/${process.env.AWS_S3_BUCKET}/${key}`; +}; diff --git a/src/services/wappconnectService.js b/src/services/wappconnectService.js new file mode 100644 index 0000000..a17d67e --- /dev/null +++ b/src/services/wappconnectService.js @@ -0,0 +1,63 @@ +const axios = require('axios'); + +const TEST_PHONES = ['9999999999']; +const TEST_OTP = '123456'; + +// Generate 6-digit OTP +function generateOTP() { + return Math.floor(100000 + Math.random() * 900000).toString(); +} + +// Send OTP via WhatsApp +async function sendOTP(phoneNumber, otp) { + // Skip real API for test phones + if (TEST_PHONES.includes(phoneNumber)) { + console.log(`๐Ÿงช Test phone โ€” Use OTP: ${TEST_OTP}`); + return { success: true, test: true }; + } + + try { + // Format phone number to 12 digits (91XXXXXXXXXX) + const digits = phoneNumber.replace(/\D/g, ''); + const formatted = + digits.startsWith('91') && digits.length === 12 ? digits : `91${digits}`; + + // โœ… Same variable name as karaychakra: WAPP_TOKEN + const WAPP_TOKEN = process.env.WAPP_TOKEN; + // const message = `๐Ÿ” Your OTP is: *${otp}*\n\nValid for 5 minutes. Do not share with anyone.`; + const message = `๐ŸŒธ *Vaishnavi Creation* ๐ŸŒธ + +To continue your login, please use the OTP below: + +๐Ÿ”‘ *${otp}* + +โณ Valid for the next *5 minutes*. + +If you did not request this OTP, please ignore this message. + +Thank you for choosing Vaishnavi Creation. ๐Ÿ’ซ`; + + console.log('๐Ÿ“ž Sending to:', formatted); + console.log( + '๐Ÿ”‘ WAPP_TOKEN:', + WAPP_TOKEN ? WAPP_TOKEN.substring(0, 10) + '...' : 'โŒ MISSING' + ); + + // โœ… Exact same logic as karaychakra + const response = await axios({ + method: 'get', + url: `https://api.wappconnect.com/api/sendText?token=${WAPP_TOKEN}&phone=${formatted}&message=${message}`, + headers: { 'Content-Type': 'application/json' }, + }); + + console.log('โœ… WhatsApp sent:', response.data); + return { success: true, data: response.data }; + } catch (error) { + console.error('โŒ Status:', error?.response?.status); + console.error('โŒ Body:', JSON.stringify(error?.response?.data)); + console.error('โŒ Real error:', error.message); + throw new Error('Failed to send WhatsApp OTP'); + } +} + +module.exports = { generateOTP, sendOTP, TEST_OTP, TEST_PHONES }; diff --git a/src/utils/mailer.js b/src/utils/mailer.js new file mode 100644 index 0000000..e9536bd --- /dev/null +++ b/src/utils/mailer.js @@ -0,0 +1,34 @@ +const nodemailer = require('nodemailer'); +const ejs = require('ejs'); +const path = require('path'); + +const transporter = nodemailer.createTransport({ + service: 'gmail', + auth: { + user: process.env.EMAIL_USER, + pass: process.env.EMAIL_PASS, + }, +}); + +const sendEmail = async (to, subject, templateName, templateData) => { + // Render EJS template + const templatePath = path.join(__dirname, '../views/emails', `${templateName}.ejs`); + const html = await ejs.renderFile(templatePath, templateData); + + const mailOptions = { + from: `"VC E-Commerce" <${process.env.EMAIL_USER}>`, + to, + subject, + html, + }; + + try { + await transporter.sendMail(mailOptions); + console.log('Email sent to', to); + } catch (err) { + console.error('Error sending email', err); + throw new Error('Failed to send email'); + } +}; + +module.exports = sendEmail; diff --git a/src/utils/paytm.js b/src/utils/paytm.js new file mode 100644 index 0000000..962e19f --- /dev/null +++ b/src/utils/paytm.js @@ -0,0 +1,315 @@ +// utils/paytm.js +const https = require('https'); +const crypto = require('crypto'); + +/** + * Paytm Configuration + * Add these to your .env file: + * PAYTM_MERCHANT_ID=your_merchant_id + * PAYTM_MERCHANT_KEY=your_merchant_key + * PAYTM_WEBSITE=WEBSTAGING (for staging) or your website name + * PAYTM_CHANNEL_ID=WEB + * PAYTM_INDUSTRY_TYPE=Retail + * PAYTM_HOST=securegw-stage.paytm.in (for staging) or securegw.paytm.in (for production) + */ + +const PaytmConfig = { + mid: process.env.PAYTM_MERCHANT_ID, + key: process.env.PAYTM_MERCHANT_KEY, + website: process.env.PAYTM_WEBSITE || 'WEBSTAGING', + channelId: process.env.PAYTM_CHANNEL_ID || 'WEB', + industryType: process.env.PAYTM_INDUSTRY_TYPE || 'Retail', + host: process.env.PAYTM_HOST || 'securegw-stage.paytm.in', + callbackUrl: process.env.PAYTM_CALLBACK_URL || 'http://localhost:3000/api/payments/paytm/callback', +}; +console.log( + 'Merchant Key Length:', + process.env.PAYTM_MERCHANT_KEY.length +); + +/** + * Generate Paytm Checksum + */ +const generateChecksum = (params, merchantKey) => { + return new Promise((resolve, reject) => { + try { + const data = JSON.stringify(params); + + const salt = crypto.randomBytes(4).toString('hex'); + const hash = crypto + .createHash('sha256') + .update(data + salt) + .digest('hex'); + + const checksum = hash + salt; + const encryptedChecksum = encrypt(checksum, merchantKey); + + resolve(encryptedChecksum); + } catch (err) { + reject(err); + } + }); +}; + + +/** + * Verify Paytm Checksum + */ +const verifyChecksum = (params, merchantKey, checksumHash) => { + return new Promise((resolve, reject) => { + try { + const decrypted = decrypt(checksumHash, merchantKey); + const salt = decrypted.slice(-8); + + const hash = crypto + .createHash('sha256') + .update(JSON.stringify(params) + salt) + .digest('hex'); + + resolve(hash + salt === decrypted); + } catch (err) { + reject(err); + } + }); +}; + + + +/** + * Encrypt data using AES-128-CBC (Paytm standard) + */ +const encrypt = (data, key) => { + if (key.length !== 16) { + throw new Error('Paytm Merchant Key must be exactly 16 characters'); + } + + const iv = Buffer.from('@@@@&&&&####$$$$'); // Paytm fixed IV + const cipher = crypto.createCipheriv( + 'aes-128-cbc', + Buffer.from(key, 'utf8'), + iv + ); + + let encrypted = cipher.update(data, 'utf8', 'base64'); + encrypted += cipher.final('base64'); + return encrypted; +}; + +/** + * Decrypt data using AES-128-CBC + */ +const decrypt = (encryptedData, key) => { + const iv = Buffer.from('@@@@&&&&####$$$$'); + const decipher = crypto.createDecipheriv( + 'aes-128-cbc', + Buffer.from(key, 'utf8'), + iv + ); + + let decrypted = decipher.update(encryptedData, 'base64', 'utf8'); + decrypted += decipher.final('utf8'); + return decrypted; +}; + + +/** + * Initiate Paytm Transaction + */ +const initiateTransaction = async (orderId, amount, customerId, email, mobile) => { + const paytmParams = { + body: { + requestType: 'Payment', + mid: PaytmConfig.mid, + websiteName: PaytmConfig.website, + orderId: orderId, + callbackUrl: PaytmConfig.callbackUrl, + txnAmount: { + value: amount.toString(), + currency: 'INR', + }, + userInfo: { + custId: customerId, + email: email, + mobile: mobile, + }, + }, + }; + + const checksum = await generateChecksum( + JSON.stringify(paytmParams.body), + PaytmConfig.key + ); + + paytmParams.head = { + signature: checksum, + }; + + return new Promise((resolve, reject) => { + const options = { + hostname: PaytmConfig.host, + port: 443, + path: `/theia/api/v1/initiateTransaction?mid=${PaytmConfig.mid}&orderId=${orderId}`, + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + }; + + const req = https.request(options, (res) => { + let data = ''; + + res.on('data', (chunk) => { + data += chunk; + }); + + res.on('end', () => { + try { + const response = JSON.parse(data); + resolve({ + success: true, + txnToken: response.body.txnToken, + orderId: orderId, + amount: amount, + ...response, + }); + } catch (error) { + reject(error); + } + }); + }); + + req.on('error', (error) => { + reject(error); + }); + + req.write(JSON.stringify(paytmParams)); + req.end(); + }); +}; + +/** + * Check Transaction Status + */ +const checkTransactionStatus = async (orderId) => { + const paytmParams = { + body: { + mid: PaytmConfig.mid, + orderId: orderId, + }, + }; + + const checksum = await generateChecksum( + JSON.stringify(paytmParams.body), + PaytmConfig.key + ); + + paytmParams.head = { + signature: checksum, + }; + + return new Promise((resolve, reject) => { + const options = { + hostname: PaytmConfig.host, + port: 443, + path: `/v3/order/status`, + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + }; + + const req = https.request(options, (res) => { + let data = ''; + + res.on('data', (chunk) => { + data += chunk; + }); + + res.on('end', () => { + try { + const response = JSON.parse(data); + resolve(response); + } catch (error) { + reject(error); + } + }); + }); + + req.on('error', (error) => { + reject(error); + }); + + req.write(JSON.stringify(paytmParams)); + req.end(); + }); +}; + +/** + * Process Refund + */ +const processRefund = async (orderId, refId, txnId, amount) => { + const paytmParams = { + body: { + mid: PaytmConfig.mid, + orderId: orderId, + refId: refId, + txnId: txnId, + txnType: 'REFUND', + refundAmount: amount.toString(), + }, + }; + + const checksum = await generateChecksum( + JSON.stringify(paytmParams.body), + PaytmConfig.key + ); + + paytmParams.head = { + signature: checksum, + }; + + return new Promise((resolve, reject) => { + const options = { + hostname: PaytmConfig.host, + port: 443, + path: `/refund/apply`, + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + }; + + const req = https.request(options, (res) => { + let data = ''; + + res.on('data', (chunk) => { + data += chunk; + }); + + res.on('end', () => { + try { + const response = JSON.parse(data); + resolve(response); + } catch (error) { + reject(error); + } + }); + }); + + req.on('error', (error) => { + reject(error); + }); + + req.write(JSON.stringify(paytmParams)); + req.end(); + }); +}; + +module.exports = { + PaytmConfig, + generateChecksum, + verifyChecksum, + initiateTransaction, + checkTransactionStatus, + processRefund, +}; \ No newline at end of file diff --git a/src/utils/uploadToS3.js b/src/utils/uploadToS3.js new file mode 100644 index 0000000..bdfe966 --- /dev/null +++ b/src/utils/uploadToS3.js @@ -0,0 +1,23 @@ +const { PutObjectCommand } = require("@aws-sdk/client-s3"); +const s3 = require("../config/s3"); +const { v4: uuidv4 } = require("uuid"); + +const uploadToS3 = async (file, folder = "products") => { + const ext = file.originalname.split(".").pop(); + const key = `${folder}/${uuidv4()}.${ext}`; + + await s3.send( + new PutObjectCommand({ + Bucket: process.env.AWS_S3_BUCKET, + Key: key, + Body: file.buffer, + ContentType: file.mimetype, + }) + ); + +// return `${process.env.AWS_ENDPOINT}/${process.env.AWS_S3_BUCKET}/${key}`; +return `https://${process.env.AWS_ENDPOINT}/${process.env.AWS_S3_BUCKET}/${key}`; + +}; + +module.exports = uploadToS3; diff --git a/src/utils/whatsapp.js b/src/utils/whatsapp.js new file mode 100644 index 0000000..32aa7a3 --- /dev/null +++ b/src/utils/whatsapp.js @@ -0,0 +1,35 @@ +// utils/whatsapp.js +const axios = require('axios'); + +const sendOTP = async (phone, otp) => { + const baseUrl = process.env.WAPP_BASE_URL; + const token = process.env.WAPP_TOKEN; + const instance = process.env.WAPP_INSTANCE; // SSM + + const formattedPhone = `${phone}@c.us`; + const message = `Your OTP is: *${otp}*\nValid for 5 minutes.`; + + // ๐Ÿ‘‡ These 3 lines will tell us exactly what's wrong + console.log('BASE_URL:', baseUrl); + console.log('TOKEN:', token ? token.substring(0, 10) + '...' : 'MISSING โŒ'); + console.log('INSTANCE:', instance || 'MISSING โŒ'); + + const url = `${baseUrl}/api/${token}/${instance}/send-message`; + console.log('Calling URL:', url); + + try { + const response = await axios.post(url, + { phone: formattedPhone, message }, + { headers: { 'Content-Type': 'application/json' } } + ); + console.log('โœ… WappConnect response:', JSON.stringify(response.data)); + return response.data; + } catch (err) { + console.log('โŒ Status:', err.response?.status); + console.log('โŒ Response body:', JSON.stringify(err.response?.data)); + console.log('โŒ Full URL was:', url); + throw new Error('Failed to send WhatsApp OTP'); + } +}; + +module.exports = { sendOTP }; \ No newline at end of file diff --git a/src/views/emails/reset-password.ejs b/src/views/emails/reset-password.ejs new file mode 100644 index 0000000..83c4a39 --- /dev/null +++ b/src/views/emails/reset-password.ejs @@ -0,0 +1,35 @@ + + + + + Password Reset + + + + + + + + + + + + +
+ VC E-Commerce +
+

Password Reset Request

+

Hi <%= firstName %>,

+

You recently requested to reset your password. Click the button below to reset it. This link is valid for 1 hour only.

+

+ + Reset Password + +

+

If you did not request a password reset, please ignore this email.

+

Thanks,
The VC E-Commerce Team

+
+ © <%= new Date().getFullYear() %> VC E-Commerce. All rights reserved. +
+ + diff --git a/src/views/emails/verify-email.ejs b/src/views/emails/verify-email.ejs new file mode 100644 index 0000000..9ff5342 --- /dev/null +++ b/src/views/emails/verify-email.ejs @@ -0,0 +1,35 @@ + + + + + Email Verification + + + + + + + + + + + + +
+ VC E-Commerce +
+

Email Verification

+

Hi <%= firstName %>,

+

Thank you for registering! Please click the button below to verify your email address and activate your account.

+

+ + Verify Email + +

+

If you did not register, you can safely ignore this email.

+

Thanks,
The VC E-Commerce Team

+
+ © <%= new Date().getFullYear() %> VC E-Commerce. All rights reserved. +
+ + diff --git a/structure.txt b/structure.txt new file mode 100644 index 0000000000000000000000000000000000000000..fff07b3d42c46218bd3b5f909861d969f96872f8 GIT binary patch literal 1220072 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