From 09ea6d4efbcbf65ed15a5665527e8d88ffc613ec Mon Sep 17 00:00:00 2001 From: tusuii Date: Thu, 19 Feb 2026 17:25:38 +0530 Subject: [PATCH] first commit --- .env.example | 19 + .eslintrc.js | 37 + .gitignore | 132 + .prettierrc | 11 + CouponGuide.md | 586 + 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 | 260 + src/controllers/admin/couponController.js | 463 + src/controllers/admin/dashboardController.js | 826 ++ src/controllers/admin/orderController.js | 395 + src/controllers/admin/productController.js | 692 ++ src/controllers/admin/reportController.js | 411 + src/controllers/admin/userController.js | 160 + src/controllers/authController.js | 297 + 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 | 264 + src/models/mongodb/Wardrobe.js | 254 + src/routes/admin.js | 324 + src/routes/auth.js | 286 + 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 | 23 + src/routes/upload.routes.js | 17 + src/routes/users.js | 550 + src/routes/wardrobe.js | 393 + src/scripts/migrateCategoryIds.js | 48 + src/server.js | 130 + src/services/authService.js | 348 + src/services/deliveryEstimationService.js | 266 + src/services/inventoryService.js | 195 + src/services/s3Upload.service.js | 25 + src/utils/mailer.js | 34 + src/utils/paytm.js | 315 + src/utils/uploadToS3.js | 23 + src/views/emails/reset-password.ejs | 35 + src/views/emails/verify-email.ejs | 35 + structure.txt | Bin 0 -> 1220072 bytes test-connections.js | 119 + 72 files changed, 24296 insertions(+) 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 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/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/s3Upload.service.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/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 diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..b1e2370 --- /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..fafc1ae --- /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..f82d99b --- /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..ba08276 --- /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..ccf723d --- /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/Inventory.md b/Inventory.md new file mode 100644 index 0000000..d19e717 --- /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..b23f807 --- /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..546e8f1 --- /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..d2b2a1c --- /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.7.9", + "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.12.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.12.2.tgz", + "integrity": "sha512-vMJzPewAlRyOgxV2dU0Cuz2O8zzzx9VYtbJOaBgXFeLc4IV/Eg50n4LowmehOOR61S8ZMpc2K5Sa7g6A4jfkUw==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.4", + "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.4", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz", + "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==", + "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..1854623 --- /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.7.9", + "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..21ce35c --- /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..7fbae53 --- /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..72f95e0 --- /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..540b486 --- /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..10d2ae3 --- /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..c456ee9 --- /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..8330194 --- /dev/null +++ b/src/controllers/admin/categoryController.js @@ -0,0 +1,260 @@ +const { prisma } = require('../../config/database'); + +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.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..caa8bc6 --- /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..a3c2e65 --- /dev/null +++ b/src/controllers/admin/dashboardController.js @@ -0,0 +1,826 @@ +// const { prisma } = require('../../config/database'); +// const Product = require('../../models/mongodb/Product'); + +// exports.getDashboardStats = async (req, res, next) => { +// try { +// const [ +// totalUsers, +// totalOrders, +// totalProducts, +// totalRevenue, +// recentOrders, +// topProducts, +// ] = await Promise.all([ +// prisma.user.count(), +// prisma.order.count(), +// Product.countDocuments(), +// 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, +// }, +// }), +// Product.find({ status: 'active' }) +// .sort({ purchaseCount: -1 }) +// .limit(5) +// .lean() +// .then(products => +// products.map(product => { +// // โœ… Fixed: Check gallery first since that's where your images are +// let image = +// (product.images?.gallery && product.images.gallery.length > 0 +// ? product.images.gallery[0] +// : null) || +// product.images?.primary || +// (product.variants?.[0]?.images && product.variants[0].images.length > 0 +// ? product.variants[0].images[0] +// : null) || +// 'https://via.placeholder.com/300'; + +// return { +// ...product, +// displayImage: image, +// }; +// }) +// ), +// ]); + +// // โœ… NEW: Fetch product details for recent orders +// const orderProductIds = recentOrders +// .flatMap(order => order.items.map(item => item.productId)) +// .filter(Boolean); + +// const orderProducts = await Product.find({ +// _id: { $in: orderProductIds } +// }).lean(); + +// // โœ… Create a map of productId -> product for quick lookup +// const productMap = {}; +// orderProducts.forEach(product => { +// const image = +// (product.images?.gallery && product.images.gallery.length > 0 +// ? product.images.gallery[0] +// : null) || +// product.images?.primary || +// (product.variants?.[0]?.images && product.variants[0].images.length > 0 +// ? product.variants[0].images[0] +// : null) || +// 'https://via.placeholder.com/300'; + +// productMap[product._id.toString()] = { +// ...product, +// displayImage: image, +// }; +// }); + +// // โœ… Enhance recent orders with product images +// 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: { +// totalUsers, +// totalOrders, +// totalProducts, +// totalRevenue: totalRevenue._sum.totalAmount || 0, +// recentOrders: enhancedRecentOrders, +// topProducts, +// }, +// }); +// } catch (error) { +// next(error); +// } +// }; + + + +// controllers/admin/dashboardController.js - WITH ORDER OVERVIEW GRAPHS + +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, +// topProducts, +// orderOverview, +// revenueOverview, +// ordersByStatus, +// monthlyComparison, +// ] = await Promise.all([ +// // Total Users +// prisma.user.count(), + +// // Total Orders +// prisma.order.count(), + +// // Total Products +// Product.countDocuments(), + +// // Total Revenue +// prisma.order.aggregate({ +// _sum: { totalAmount: true }, +// where: { paymentStatus: 'PAID' }, +// }), + +// // Recent Orders +// prisma.order.findMany({ +// take: 5, +// orderBy: { createdAt: 'desc' }, +// include: { +// user: { +// select: { id: true, email: true, firstName: true, lastName: true }, +// }, +// items: true, +// }, +// }), + +// // Top Products +// Product.find({ status: 'active' }) +// .sort({ purchaseCount: -1 }) +// .limit(5) +// .lean() +// .then(products => +// products.map(product => { +// let image = +// (product.images?.gallery && product.images.gallery.length > 0 +// ? product.images.gallery[0] +// : null) || +// product.images?.primary || +// (product.variants?.[0]?.images && product.variants[0].images.length > 0 +// ? product.variants[0].images[0] +// : null) || +// 'https://via.placeholder.com/300'; + +// return { +// ...product, +// displayImage: image, +// }; +// }) +// ), + +// // โœ… Order Overview (Last 30 Days) +// getOrderOverview(), + +// // โœ… Revenue Overview (Last 30 Days) +// getRevenueOverview(), + +// // โœ… Orders by Status +// getOrdersByStatus(), + +// // โœ… Monthly Comparison (Current vs Previous Month) +// 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 => { +// const image = +// (product.images?.gallery && product.images.gallery.length > 0 +// ? product.images.gallery[0] +// : null) || +// product.images?.primary || +// (product.variants?.[0]?.images && product.variants[0].images.length > 0 +// ? product.variants[0].images[0] +// : null) || +// 'https://via.placeholder.com/300'; + +// productMap[product._id.toString()] = { +// ...product, +// displayImage: image, +// }; +// }); + +// 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), + +// // Lists +// recentOrders: enhancedRecentOrders, +// topProducts, + +// // โœ… Graph Data +// charts: { +// orderOverview, +// revenueOverview, +// ordersByStatus, +// monthlyComparison, +// }, +// }, +// }); +// } catch (error) { +// console.error('Dashboard stats error:', error); +// next(error); +// } +// }; + +// ========================================== +// HELPER FUNCTIONS FOR GRAPHS +// ========================================== + + + +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, + }; + }); + + const topProducts = products + .map(product => { + const stats = statsMap[product._id.toString()] || { totalSold: 0, totalOrders: 0 }; + const stock = 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: stock, // โœ… Current stock + stockStatus: getStockStatus(stock), // โœ… Stock status + }; + }) + .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..cde653a --- /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..41505b9 --- /dev/null +++ b/src/controllers/admin/productController.js @@ -0,0 +1,692 @@ +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.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..c69ba34 --- /dev/null +++ b/src/controllers/admin/reportController.js @@ -0,0 +1,411 @@ +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); + } + }, +}; diff --git a/src/controllers/admin/userController.js b/src/controllers/admin/userController.js new file mode 100644 index 0000000..882dfae --- /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..6a90100 --- /dev/null +++ b/src/controllers/authController.js @@ -0,0 +1,297 @@ +const authService = require('../services/authService'); + +// @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', + }); + } +}; + +// @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..93f2405 --- /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..95d5e69 --- /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..00b92ea --- /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..18201d4 --- /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..b4d0dc3 --- /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..8c78167 --- /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..013918d --- /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..dd9538c --- /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..7b9d951 --- /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..b861f8d --- /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..dd64688 --- /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..8451ce6 --- /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..a55fab7 --- /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..3a53ab9 --- /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..4a6b8fc --- /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..0d38ffc --- /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..0f84495 --- /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..c6d6165 --- /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..819e0a3 --- /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..123a2ed --- /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..8f14205 --- /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..ff5dd33 --- /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..bdfd570 --- /dev/null +++ b/src/models/mongodb/Product.js @@ -0,0 +1,264 @@ +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 }); + +// 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) + ); +}; + +// 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..0687192 --- /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..ca40cfb --- /dev/null +++ b/src/routes/admin.js @@ -0,0 +1,324 @@ +// 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.get('/categories/:id', categories.getCategoryById); +router.put('/categories/:id', 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; \ No newline at end of file diff --git a/src/routes/auth.js b/src/routes/auth.js new file mode 100644 index 0000000..ebee04e --- /dev/null +++ b/src/routes/auth.js @@ -0,0 +1,286 @@ +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', async (req, res, next) => { +// try { +// const { email, password, firstName, lastName, username, phone } = req.body; + +// // Validation +// if (!email || !password) { +// return res.status(400).json({ +// success: false, +// message: 'Email and password are required', +// }); +// } + +// if (password.length < 6) { +// return res.status(400).json({ +// success: false, +// message: 'Password must be at least 6 characters', +// }); +// } + +// const result = await authService.register({ +// email, +// password, +// firstName, +// lastName, +// username, +// phone, +// }); + +// res.status(201).json({ +// success: true, +// message: 'User registered successfully', +// data: result, +// }); +// } catch (error) { +// next(error); +// } +// }); + +router.post('/register', authController.register); + +// @desc Login user +// @route POST /api/auth/login +// @access Public +// router.post('/login', async (req, res, next) => { +// try { +// const { email, password } = req.body; + +// // Validation +// if (!email || !password) { +// return res.status(400).json({ +// success: false, +// message: 'Email and password are required', +// }); +// } + +// const result = await authService.login(email, password); + +// res.json({ +// success: true, +// message: 'Login successful', +// data: result, +// }); +// } catch (error) { +// next(error); +// } +// }); + +router.post('/login', authController.login); + +// @desc Refresh token +// @route POST /api/auth/refresh +// @access Public +// router.post('/refresh', async (req, res, next) => { +// try { +// const { refreshToken } = req.body; + +// if (!refreshToken) { +// return res.status(400).json({ +// success: false, +// message: 'Refresh token is required', +// }); +// } + +// const result = await authService.refreshToken(refreshToken); + +// res.json({ +// success: true, +// message: 'Token refreshed successfully', +// data: result, +// }); +// } catch (error) { +// next(error); +// } +// }); + +router.post('/refresh', authController.refreshToken); + +// @desc Logout user +// @route POST /api/auth/logout +// @access Private +// router.post('/logout', protect, async (req, res, next) => { +// try { +// await authService.logout(req.user.id); + +// res.json({ +// success: true, +// message: 'Logout successful', +// }); +// } catch (error) { +// next(error); +// } +// }); + +router.post('/logout', protect, authController.logout); + +// @desc Change password +// @route PUT /api/auth/change-password +// @access Private +// router.put('/change-password', protect, async (req, res, next) => { +// try { +// const { currentPassword, newPassword } = req.body; + +// // Validation +// if (!currentPassword || !newPassword) { +// return res.status(400).json({ +// success: false, +// message: 'Current password and new password are required', +// }); +// } + +// if (newPassword.length < 6) { +// return res.status(400).json({ +// success: false, +// message: 'New password must be at least 6 characters', +// }); +// } + +// await authService.changePassword(req.user.id, currentPassword, newPassword); + +// res.json({ +// success: true, +// message: 'Password changed successfully', +// }); +// } catch (error) { +// next(error); +// } +// }); + +router.put('/change-password', protect, authController.changePassword); + +// @desc Request password reset +// @route POST /api/auth/forgot-password +// @access Public +// router.post('/forgot-password', async (req, res, next) => { +// try { +// const { email } = req.body; + +// if (!email) { +// return res.status(400).json({ +// success: false, +// message: 'Email is required', +// }); +// } + +// const result = await authService.requestPasswordReset(email); + +// res.json({ +// success: true, +// message: result.message, +// }); +// } catch (error) { +// next(error); +// } +// }); + +router.post('/forgot-password', authController.forgotPassword); + +// @desc Reset password with token +// @route POST /api/auth/reset-password +// @access Public +// router.post('/reset-password', async (req, res, next) => { +// try { +// const { token, newPassword } = req.body; + +// // Validation +// if (!token || !newPassword) { +// return res.status(400).json({ +// success: false, +// message: 'Token and new password are required', +// }); +// } + +// if (newPassword.length < 6) { +// return res.status(400).json({ +// success: false, +// message: 'Password must be at least 6 characters', +// }); +// } + +// await authService.resetPassword(token, newPassword); + +// res.json({ +// success: true, +// message: 'Password reset successfully', +// }); +// } catch (error) { +// next(error); +// } +// }); + +router.post('/reset-password', authController.resetPassword); + +// @desc Send verification email +// @route POST /api/auth/send-verification +// @access Private +// router.post('/send-verification', protect, async (req, res, next) => { +// try { +// await authService.sendVerificationEmail(req.user.id); + +// res.json({ +// success: true, +// message: 'Verification email sent', +// }); +// } catch (error) { +// next(error); +// } +// }); + +router.post('/send-verification', protect, authController.sendVerification); + +// @desc Verify email with token +// @route POST /api/auth/verify-email +// @access Public +// router.post('/verify-email', async (req, res, next) => { +// try { +// const { token } = req.body; + +// if (!token) { +// return res.status(400).json({ +// success: false, +// message: 'Verification token is required', +// }); +// } + +// await authService.verifyEmail(token); + +// res.json({ +// success: true, +// message: 'Email verified successfully', +// }); +// } catch (error) { +// next(error); +// } +// }); + +router.post('/verify-email', authController.verifyEmail); + +// @desc Get current user profile +// @route GET /api/auth/me +// @access Private +// router.get('/me', protect, async (req, res, next) => { +// try { +// res.json({ +// success: true, +// data: { +// user: req.user, +// }, +// }); +// } catch (error) { +// next(error); +// } +// }); + + +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..c292bfb --- /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..e2d329c --- /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..59a8ec0 --- /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..ab96b0d --- /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..e4bc6f4 --- /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..27733a6 --- /dev/null +++ b/src/routes/reports.js @@ -0,0 +1,23 @@ +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("/returns", reports.getReturnAnalytics); +router.get("/inventory", reports.getInventoryStats); +router.get("/financial", reports.getFinancialStats); +// router.get("/payout-history", reports.getPayoutHistory); +// router.get("/activity", reports.getActivityFeed); + +module.exports = router; diff --git a/src/routes/upload.routes.js b/src/routes/upload.routes.js new file mode 100644 index 0000000..b4619a0 --- /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..10f2976 --- /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..134aec7 --- /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/migrateCategoryIds.js b/src/scripts/migrateCategoryIds.js new file mode 100644 index 0000000..1ba5454 --- /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..486484a --- /dev/null +++ b/src/server.js @@ -0,0 +1,130 @@ +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, + }) +); + +// CORS configuration +const corsOptions = { + origin: process.env.CORS_ORIGIN?.split(',') || [ + 'http://localhost:3000', + 'http://localhost:3001', + 'http://localhost:5173', + 'http://localhost:5174', + ], + 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', (req, res) => { + res.status(200).json({ + status: 'OK', + timestamp: new Date().toISOString(), + uptime: process.uptime(), + environment: process.env.NODE_ENV, + }); +}); + +// 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..95a8410 --- /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..ca50cc8 --- /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..b7554ee --- /dev/null +++ b/src/services/inventoryService.js @@ -0,0 +1,195 @@ +// 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 { + console.log('๐Ÿ“ฆ Reducing stock for order:', orderId); + + const order = await prisma.order.findUnique({ + where: { id: orderId }, + include: { items: true }, + }); + + if (!order || order.status !== 'DELIVERED') { + console.log('โš ๏ธ Order not delivered, skipping stock reduction'); + return null; + } + + const results = []; + + for (const item of order.items) { + const product = await Product.findById(item.productId); + + if (!product) { + console.log(`โŒ Product not found: ${item.productId}`); + continue; + } + + const currentStock = product.stock || 0; + const newStock = Math.max(0, currentStock - item.quantity); + + // Update stock in MongoDB + await Product.findByIdAndUpdate(item.productId, { + stock: newStock, + updatedAt: new Date(), + }); + + // Create inventory log + await prisma.inventoryLog.create({ + data: { + productId: item.productId, + productName: item.productName || product.name, + type: 'SOLD', + quantityChange: -item.quantity, + previousStock: currentStock, + newStock: newStock, + orderId: orderId, + notes: `Order ${order.orderNumber} delivered`, + }, + }); + + console.log(`โœ… ${product.name}: ${currentStock} โ†’ ${newStock} (-${item.quantity})`); + + results.push({ + productId: item.productId, + productName: product.name, + reduced: item.quantity, + previousStock: currentStock, + newStock: 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', + stock: { $lte: threshold }, + }) + .select('name slug stock basePrice images') + .sort({ stock: 1 }) + .lean(); + + return products.map(product => ({ + _id: product._id.toString(), + name: product.name, + slug: product.slug, + stock: product.stock || 0, + basePrice: product.basePrice, + status: product.stock === 0 ? 'OUT_OF_STOCK' : product.stock <= 5 ? 'CRITICAL' : 'LOW', + displayImage: getProductImage(product), + })); + } catch (error) { + console.error('Error fetching low stock:', error); + return []; + } +} + +/** + * โœ… Get inventory stats for dashboard + */ +async function getInventoryStats() { + try { + const [totalProducts, outOfStock, criticalStock, lowStock] = await Promise.all([ + Product.countDocuments({ status: 'active' }), + Product.countDocuments({ status: 'active', stock: 0 }), + Product.countDocuments({ status: 'active', stock: { $gte: 1, $lte: 5 } }), + Product.countDocuments({ status: 'active', stock: { $gte: 6, $lte: 10 } }), + ]); + + return { + totalProducts, + outOfStock, + criticalStock, + lowStock, + inStock: totalProducts - outOfStock - criticalStock - lowStock, + }; + } catch (error) { + console.error('Error fetching inventory stats:', error); + return null; + } +} + +/** + * โœ… Manual stock adjustment (Admin) + */ +async function adjustStock(productId, quantity, type, notes, adminId) { + try { + const product = await Product.findById(productId); + if (!product) throw new Error('Product not found'); + + const currentStock = product.stock || 0; + let newStock; + + switch (type) { + case 'ADD': + newStock = currentStock + quantity; + break; + case 'REMOVE': + newStock = Math.max(0, currentStock - quantity); + break; + case 'SET': + newStock = quantity; + break; + default: + 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: currentStock, + newStock: newStock, + notes: notes || `Manual ${type} by admin`, + adjustedBy: adminId, + }, + }); + + return { + success: true, + productName: product.name, + previousStock: currentStock, + newStock: 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, +}; \ 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..610f2b5 --- /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/utils/mailer.js b/src/utils/mailer.js new file mode 100644 index 0000000..8e92889 --- /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..0c92728 --- /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..26c7bde --- /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/views/emails/reset-password.ejs b/src/views/emails/reset-password.ejs new file mode 100644 index 0000000..083f026 --- /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..b4a5680 --- /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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