commit 09ea6d4efbcbf65ed15a5665527e8d88ffc613ec Author: tusuii Date: Thu Feb 19 17:25:38 2026 +0530 first commit 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 0000000..fff07b3 Binary files /dev/null and b/structure.txt differ diff --git a/test-connections.js b/test-connections.js new file mode 100644 index 0000000..e5f42b3 --- /dev/null +++ b/test-connections.js @@ -0,0 +1,119 @@ +#!/usr/bin/env node + +/** + * Database Connection Test Script + * + * This script tests the database connections to help troubleshoot setup issues. + */ + +require('dotenv').config(); + +const { PrismaClient } = require('@prisma/client'); +const mongoose = require('mongoose'); + +async function testPostgreSQL() { + console.log('๐Ÿ˜ Testing PostgreSQL connection...'); + + const prisma = new PrismaClient(); + + try { + await prisma.$connect(); + console.log('โœ… PostgreSQL connection successful!'); + + // Test a simple query + const result = await prisma.$queryRaw`SELECT version()`; + console.log('๐Ÿ“Š PostgreSQL version:', result[0]?.version || 'Unknown'); + + return true; + } catch (error) { + console.log('โŒ PostgreSQL connection failed:', error.message); + console.log('๐Ÿ’ก Check your DATABASE_URL in .env file'); + return false; + } finally { + await prisma.$disconnect(); + } +} + +async function testMongoDB() { + console.log('\n๐Ÿƒ Testing MongoDB connection...'); + + try { + await mongoose.connect(process.env.MONGODB_URI, { + useNewUrlParser: true, + useUnifiedTopology: true, + }); + + console.log('โœ… MongoDB connection successful!'); + + // Test a simple operation + const adminDb = mongoose.connection.db.admin(); + const buildInfo = await adminDb.buildInfo(); + console.log('๐Ÿ“Š MongoDB version:', buildInfo.version); + + return true; + } catch (error) { + console.log('โŒ MongoDB connection failed:', error.message); + console.log('๐Ÿ’ก Check your MONGODB_URI in .env file'); + console.log('๐Ÿ’ก Make sure MongoDB is running on port 27017'); + return false; + } finally { + await mongoose.connection.close(); + } +} + +async function testRedis() { + console.log('\n๐Ÿ”ด Testing Redis connection...'); + + try { + const Redis = require('ioredis'); + const redis = new Redis(process.env.REDIS_URL); + + await redis.ping(); + console.log('โœ… Redis connection successful!'); + + const info = await redis.info('server'); + const version = info.match(/redis_version:([^\r\n]+)/); + if (version) { + console.log('๐Ÿ“Š Redis version:', version[1]); + } + + redis.disconnect(); + return true; + } catch (error) { + console.log('โŒ Redis connection failed:', error.message); + console.log('๐Ÿ’ก Check your REDIS_URL in .env file'); + console.log('๐Ÿ’ก Make sure Redis is running on port 6379'); + console.log('๐Ÿ’ก You can comment out REDIS_URL if you don\'t need Redis'); + return false; + } +} + +async function main() { + console.log('๐Ÿ” Testing database connections...\n'); + + const results = { + postgresql: await testPostgreSQL(), + mongodb: await testMongoDB(), + redis: await testRedis(), + }; + + console.log('\n๐Ÿ“‹ Connection Summary:'); + console.log(`PostgreSQL: ${results.postgresql ? 'โœ… Connected' : 'โŒ Failed'}`); + console.log(`MongoDB: ${results.mongodb ? 'โœ… Connected' : 'โŒ Failed'}`); + console.log(`Redis: ${results.redis ? 'โœ… Connected' : 'โŒ Failed'}`); + + if (results.postgresql && results.mongodb) { + console.log('\n๐ŸŽ‰ Core databases are connected! You can now run:'); + console.log(' npm run db:push # Create database schema'); + console.log(' npm run db:seed # Seed with sample data'); + console.log(' npm run dev # Start development server'); + } else { + console.log('\nโš ๏ธ Some databases failed to connect. Please check your configuration.'); + } + + if (!results.redis) { + console.log('\n๐Ÿ’ก Redis is optional. The app will work without it, but caching and background jobs won\'t be available.'); + } +} + +main().catch(console.error);