first commit
This commit is contained in:
4
.dockerignore
Normal file
4
.dockerignore
Normal file
@@ -0,0 +1,4 @@
|
||||
node_modules
|
||||
npm-debug.log
|
||||
.git
|
||||
.env
|
||||
19
.env.example
Normal file
19
.env.example
Normal file
@@ -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"
|
||||
37
.eslintrc.js
Normal file
37
.eslintrc.js
Normal file
@@ -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',
|
||||
],
|
||||
};
|
||||
132
.gitignore
vendored
Normal file
132
.gitignore
vendored
Normal file
@@ -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/
|
||||
11
.prettierrc
Normal file
11
.prettierrc
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"semi": true,
|
||||
"trailingComma": "es5",
|
||||
"singleQuote": true,
|
||||
"printWidth": 80,
|
||||
"tabWidth": 2,
|
||||
"useTabs": false,
|
||||
"bracketSpacing": true,
|
||||
"arrowParens": "avoid",
|
||||
"endOfLine": "lf"
|
||||
}
|
||||
586
CouponGuide.md
Normal file
586
CouponGuide.md
Normal file
@@ -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 (
|
||||
<div className="max-w-4xl mx-auto p-6">
|
||||
{/* Cart Items */}
|
||||
|
||||
{/* Coupon Section */}
|
||||
<CouponApply
|
||||
orderAmount={orderTotal}
|
||||
onCouponApplied={handleCouponApplied}
|
||||
onCouponRemoved={handleCouponRemoved}
|
||||
/>
|
||||
|
||||
{/* Order Summary */}
|
||||
<div className="mt-6 bg-white p-6 rounded-lg">
|
||||
<div className="flex justify-between mb-2">
|
||||
<span>Subtotal:</span>
|
||||
<span>₹2,599</span>
|
||||
</div>
|
||||
{appliedCoupon && (
|
||||
<div className="flex justify-between mb-2 text-green-600">
|
||||
<span>Discount ({appliedCoupon.couponCode}):</span>
|
||||
<span>-₹{appliedCoupon.discountAmount}</span>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex justify-between text-xl font-bold border-t pt-2">
|
||||
<span>Total:</span>
|
||||
<span>₹{orderTotal.toLocaleString()}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📡 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 <admin_token>
|
||||
```
|
||||
|
||||
**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 <admin_token>
|
||||
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 <admin_token>
|
||||
|
||||
{
|
||||
"value": 25,
|
||||
"maxUses": 150
|
||||
}
|
||||
```
|
||||
|
||||
#### 4. Toggle Coupon Status
|
||||
```http
|
||||
PATCH /api/coupons/admin/:id/toggle
|
||||
Authorization: Bearer <admin_token>
|
||||
```
|
||||
|
||||
#### 5. Get Coupon Statistics
|
||||
```http
|
||||
GET /api/coupons/admin/stats
|
||||
Authorization: Bearer <admin_token>
|
||||
```
|
||||
|
||||
**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 <token>
|
||||
|
||||
{
|
||||
"couponCode": "SAVE20",
|
||||
"orderId": "order_123"
|
||||
}
|
||||
```
|
||||
|
||||
#### 4. Remove Coupon
|
||||
```http
|
||||
POST /api/coupons/remove
|
||||
Authorization: Bearer <token>
|
||||
|
||||
{
|
||||
"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);
|
||||
|
||||
<CouponApply
|
||||
orderAmount={2599}
|
||||
onCouponApplied={(data) => {
|
||||
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 ✅
|
||||
14
Dockerfile
Normal file
14
Dockerfile
Normal file
@@ -0,0 +1,14 @@
|
||||
FROM node:20-bullseye-slim
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY package*.json ./
|
||||
RUN npm install
|
||||
|
||||
COPY . .
|
||||
|
||||
RUN npx prisma generate
|
||||
|
||||
EXPOSE 3000
|
||||
|
||||
CMD ["node", "src/server.js"]
|
||||
332
Inventory.md
Normal file
332
Inventory.md
Normal file
@@ -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 => (
|
||||
<div key={product._id} className="flex items-center gap-4 p-4 bg-white rounded-lg shadow">
|
||||
<img src={product.displayImage} className="w-16 h-16 rounded" />
|
||||
<div className="flex-1">
|
||||
<h3 className="font-bold">{product.name}</h3>
|
||||
<div className="flex items-center gap-4 text-sm text-gray-600">
|
||||
<span>Sold: {product.totalSold}</span>
|
||||
<span>Revenue: ₹{product.revenue.toLocaleString()}</span>
|
||||
<span className={`px-2 py-1 rounded text-xs ${
|
||||
product.stockStatus === 'OUT_OF_STOCK' ? 'bg-red-100 text-red-800' :
|
||||
product.stockStatus === 'CRITICAL' ? 'bg-orange-100 text-orange-800' :
|
||||
product.stockStatus === 'LOW' ? 'bg-yellow-100 text-yellow-800' :
|
||||
'bg-green-100 text-green-800'
|
||||
}`}>
|
||||
{product.stock} in stock
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
```
|
||||
|
||||
### Dashboard - Low Stock Alerts
|
||||
|
||||
```javascript
|
||||
{lowStockProducts.length > 0 && (
|
||||
<div className="bg-orange-50 border border-orange-200 rounded-lg p-6">
|
||||
<h3 className="text-lg font-bold text-orange-900 mb-4">
|
||||
⚠️ Low Stock Alert ({lowStockProducts.length})
|
||||
</h3>
|
||||
<div className="space-y-3">
|
||||
{lowStockProducts.map(product => (
|
||||
<div key={product._id} className="flex items-center justify-between p-3 bg-white rounded">
|
||||
<div className="flex items-center gap-3">
|
||||
<img src={product.displayImage} className="w-12 h-12 rounded" />
|
||||
<div>
|
||||
<p className="font-semibold">{product.name}</p>
|
||||
<p className="text-sm text-gray-600">
|
||||
{product.stock === 0 ? 'Out of Stock' : `Only ${product.stock} left`}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<button className="px-4 py-2 bg-orange-600 text-white rounded">
|
||||
Restock
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔧 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!** 📦✨
|
||||
183
LOCAL_SETUP.md
Normal file
183
LOCAL_SETUP.md
Normal file
@@ -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! 🚀
|
||||
289
README.md
Normal file
289
README.md
Normal file
@@ -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 <repository-url>
|
||||
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 <your-jwt-token>
|
||||
```
|
||||
|
||||
### 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**
|
||||
9431
package-lock.json
generated
Normal file
9431
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
69
package.json
Normal file
69
package.json
Normal file
@@ -0,0 +1,69 @@
|
||||
{
|
||||
"name": "vaishnavi-backend",
|
||||
"version": "1.0.0",
|
||||
"description": "Vaishnavi Creation - E-commerce Backend API",
|
||||
"main": "src/server.js",
|
||||
"scripts": {
|
||||
"start": "node src/server.js",
|
||||
"dev": "nodemon src/server.js",
|
||||
"build": "npm run db:generate",
|
||||
"test": "jest",
|
||||
"test:watch": "jest --watch",
|
||||
"test:coverage": "jest --coverage",
|
||||
"lint": "eslint src/",
|
||||
"lint:fix": "eslint src/ --fix",
|
||||
"format": "prettier --write src/",
|
||||
"format:check": "prettier --check src/",
|
||||
"db:generate": "prisma generate",
|
||||
"db:push": "prisma db push",
|
||||
"db:migrate": "prisma migrate dev",
|
||||
"db:deploy": "prisma migrate deploy",
|
||||
"db:studio": "prisma studio",
|
||||
"db:seed": "node prisma/seed.js",
|
||||
"setup": "node setup-local.js",
|
||||
"test:connections": "node test-connections.js"
|
||||
},
|
||||
"keywords": [
|
||||
"e-commerce",
|
||||
"fashion",
|
||||
"api",
|
||||
"nodejs",
|
||||
"express",
|
||||
"postgresql",
|
||||
"mongodb"
|
||||
],
|
||||
"author": "Vaishnavi Creation",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@prisma/client": "^5.22.0",
|
||||
"aws-sdk": "^2.1693.0",
|
||||
"axios": "^1.13.5",
|
||||
"bcrypt": "^5.1.1",
|
||||
"bullmq": "^5.25.0",
|
||||
"cookie-parser": "^1.4.7",
|
||||
"cors": "^2.8.5",
|
||||
"dotenv": "^16.4.7",
|
||||
"ejs": "^3.1.10",
|
||||
"express": "^4.21.2",
|
||||
"helmet": "^8.0.0",
|
||||
"ioredis": "^5.4.1",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"mongoose": "^8.9.2",
|
||||
"morgan": "^1.10.0",
|
||||
"multer": "^1.4.5-lts.1",
|
||||
"multer-s3": "^3.0.1",
|
||||
"nodemailer": "^7.0.11",
|
||||
"prisma": "^5.22.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"eslint": "^9.17.0",
|
||||
"jest": "^29.7.0",
|
||||
"nodemon": "^3.1.9",
|
||||
"prettier": "^3.4.2",
|
||||
"supertest": "^7.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.0.0",
|
||||
"npm": ">=8.0.0"
|
||||
}
|
||||
}
|
||||
384
prisma/schema.prisma
Normal file
384
prisma/schema.prisma
Normal file
@@ -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
|
||||
}
|
||||
174
prisma/seed.js
Normal file
174
prisma/seed.js
Normal file
@@ -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();
|
||||
});
|
||||
101
setup-local.js
Normal file
101
setup-local.js
Normal file
@@ -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');
|
||||
57
src/config/database.js
Normal file
57
src/config/database.js
Normal file
@@ -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,
|
||||
};
|
||||
5
src/config/returnPolicy.js
Normal file
5
src/config/returnPolicy.js
Normal file
@@ -0,0 +1,5 @@
|
||||
// backend/config/returnPolicy.js
|
||||
module.exports = {
|
||||
RETURN_WINDOW_DAYS: 7,
|
||||
ALLOWED_STATUSES: ['DELIVERED'],
|
||||
};
|
||||
16
src/config/s3.js
Normal file
16
src/config/s3.js
Normal file
@@ -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;
|
||||
375
src/controllers/admin/categoryController.js
Normal file
375
src/controllers/admin/categoryController.js
Normal file
@@ -0,0 +1,375 @@
|
||||
const { prisma } = require('../../config/database');
|
||||
const uploadToS3 = require('../../utils/uploadToS3');
|
||||
|
||||
exports.getAllCategories = async (req, res, next) => {
|
||||
try {
|
||||
const categories = await prisma.category.findMany({
|
||||
orderBy: { name: 'asc' },
|
||||
});
|
||||
// res.json({ success: true, data: categories });
|
||||
res.status(200).json({
|
||||
// statusCode: 200,
|
||||
status: true,
|
||||
message: 'Categories fetched successfully',
|
||||
data: categories,
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
// exports.createCategory = async (req, res, next) => {
|
||||
// try {
|
||||
// const { name, description, image, parentId, metaTitle, metaDescription } =
|
||||
// req.body;
|
||||
|
||||
// // Generate slug from name
|
||||
// let slug = name
|
||||
// .toLowerCase()
|
||||
// .replace(/[^a-z0-9]+/g, '-')
|
||||
// .replace(/(^-|-$)/g, '');
|
||||
|
||||
// // Check if slug already exists with the same parent
|
||||
// const existing = await prisma.category.findFirst({
|
||||
// where: {
|
||||
// slug,
|
||||
// parentId: parentId || null, // Handle both subcategories and root categories
|
||||
// },
|
||||
// });
|
||||
|
||||
// if (existing) {
|
||||
// // If exists under same parent, append timestamp to make it unique
|
||||
// slug = `${slug}-${Date.now()}`;
|
||||
// }
|
||||
|
||||
// const category = await prisma.category.create({
|
||||
// data: {
|
||||
// name,
|
||||
// slug,
|
||||
// description,
|
||||
// image,
|
||||
// parentId,
|
||||
// metaTitle,
|
||||
// metaDescription,
|
||||
// },
|
||||
// });
|
||||
|
||||
// res.status(201).json({
|
||||
// statusCode: 201,
|
||||
// status: true,
|
||||
// message: 'Category created successfully',
|
||||
// data: category,
|
||||
// });
|
||||
// } catch (error) {
|
||||
// // Handle Prisma duplicate error explicitly
|
||||
// if (error.code === 'P2002') {
|
||||
// return res.status(400).json({
|
||||
// statusCode: 400,
|
||||
// status: false,
|
||||
// message: 'Duplicate field value entered',
|
||||
// });
|
||||
// }
|
||||
// next(error);
|
||||
// }
|
||||
// };
|
||||
|
||||
// exports.updateCategory = async (req, res, next) => {
|
||||
// try {
|
||||
// const { id } = req.params;
|
||||
// const { name, description, image, parentId, metaTitle, metaDescription } =
|
||||
// req.body;
|
||||
|
||||
// let slug;
|
||||
// if (name) {
|
||||
// slug = name
|
||||
// .toLowerCase()
|
||||
// .replace(/[^a-z0-9]+/g, '-')
|
||||
// .replace(/(^-|-$)/g, '');
|
||||
// }
|
||||
|
||||
// const category = await prisma.category.update({
|
||||
// where: { id },
|
||||
// data: {
|
||||
// name,
|
||||
// slug,
|
||||
// description,
|
||||
// image,
|
||||
// parentId,
|
||||
// metaTitle,
|
||||
// metaDescription,
|
||||
// },
|
||||
// });
|
||||
|
||||
// res.json({
|
||||
// status: true,
|
||||
// message: 'Category updated successfully',
|
||||
// data: category,
|
||||
// });
|
||||
// } catch (error) {
|
||||
// next(error);
|
||||
// }
|
||||
// };
|
||||
|
||||
exports.createCategory = async (req, res, next) => {
|
||||
try {
|
||||
const { name, description, parentId, metaTitle, metaDescription } =
|
||||
req.body;
|
||||
|
||||
if (!name) {
|
||||
return res.status(400).json({
|
||||
status: false,
|
||||
message: 'Category name is required',
|
||||
});
|
||||
}
|
||||
|
||||
// ✅ Generate slug
|
||||
let slug = name
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9]+/g, '-')
|
||||
.replace(/(^-|-$)/g, '');
|
||||
|
||||
// ✅ Check duplicate slug under same parent
|
||||
const existing = await prisma.category.findFirst({
|
||||
where: {
|
||||
slug,
|
||||
parentId: parentId || null,
|
||||
},
|
||||
});
|
||||
|
||||
if (existing) {
|
||||
slug = `${slug}-${Date.now()}`;
|
||||
}
|
||||
|
||||
// ✅ Upload image to S3 if provided
|
||||
let imageUrl = null;
|
||||
|
||||
if (req.file) {
|
||||
imageUrl = await uploadToS3(req.file, 'categories');
|
||||
}
|
||||
|
||||
// ✅ Create category
|
||||
const category = await prisma.category.create({
|
||||
data: {
|
||||
name,
|
||||
slug,
|
||||
description,
|
||||
parentId: parentId || null,
|
||||
metaTitle,
|
||||
metaDescription,
|
||||
image: imageUrl,
|
||||
},
|
||||
});
|
||||
|
||||
res.status(201).json({
|
||||
statusCode: 201,
|
||||
status: true,
|
||||
message: 'Category created successfully',
|
||||
data: category,
|
||||
});
|
||||
} catch (error) {
|
||||
if (error.code === 'P2002') {
|
||||
return res.status(400).json({
|
||||
statusCode: 400,
|
||||
status: false,
|
||||
message: 'Duplicate field value entered',
|
||||
});
|
||||
}
|
||||
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
exports.updateCategory = async (req, res, next) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const { name, description, parentId, metaTitle, metaDescription } =
|
||||
req.body;
|
||||
|
||||
// Generate slug if name changed
|
||||
let slug;
|
||||
if (name) {
|
||||
slug = name
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9]+/g, '-')
|
||||
.replace(/(^-|-$)/g, '');
|
||||
}
|
||||
|
||||
// ✅ Upload new image if provided
|
||||
let imageUrl;
|
||||
if (req.file) {
|
||||
imageUrl = await uploadToS3(req.file, 'categories');
|
||||
}
|
||||
|
||||
// Update category
|
||||
const category = await prisma.category.update({
|
||||
where: { id },
|
||||
data: {
|
||||
...(name && { name }),
|
||||
...(slug && { slug }),
|
||||
...(description && { description }),
|
||||
...(parentId !== undefined && { parentId }),
|
||||
...(metaTitle && { metaTitle }),
|
||||
...(metaDescription && { metaDescription }),
|
||||
...(imageUrl && { image: imageUrl }), // 👈 only update if new image uploaded
|
||||
},
|
||||
});
|
||||
|
||||
res.json({
|
||||
status: true,
|
||||
message: 'Category updated successfully',
|
||||
data: category,
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
exports.deleteCategory = async (req, res, next) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
|
||||
await prisma.category.delete({
|
||||
where: { id },
|
||||
});
|
||||
|
||||
res.json({
|
||||
status: true,
|
||||
message: 'Category deleted successfully',
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
exports.toggleCategoryStatus = async (req, res, next) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const { isActive } = req.body;
|
||||
|
||||
// 1️⃣ Update parent category
|
||||
const parentCategory = await prisma.category.update({
|
||||
where: { id },
|
||||
data: { isActive },
|
||||
});
|
||||
|
||||
// 2️⃣ If parent is being deactivated, also deactivate all children recursively
|
||||
if (!isActive) {
|
||||
const deactivateChildren = async parentId => {
|
||||
const children = await prisma.category.findMany({
|
||||
where: { parentId },
|
||||
});
|
||||
|
||||
for (const child of children) {
|
||||
await prisma.category.update({
|
||||
where: { id: child.id },
|
||||
data: { isActive: false },
|
||||
});
|
||||
// Recursive call for nested subcategories
|
||||
await deactivateChildren(child.id);
|
||||
}
|
||||
};
|
||||
|
||||
await deactivateChildren(id);
|
||||
}
|
||||
|
||||
res.json({
|
||||
status: true,
|
||||
message: `Category ${isActive ? 'activated' : 'deactivated'} successfully`,
|
||||
data: parentCategory,
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
exports.reorderCategories = async (req, res) => {
|
||||
const { orders } = req.body;
|
||||
|
||||
await Promise.all(
|
||||
orders.map(item =>
|
||||
prisma.category.update({
|
||||
where: { id: item.id },
|
||||
data: { sequence: item.sequence },
|
||||
})
|
||||
)
|
||||
);
|
||||
|
||||
res.json({
|
||||
status: true,
|
||||
message: 'Category order updated',
|
||||
});
|
||||
};
|
||||
|
||||
exports.getCategoryHierarchy = async (req, res, next) => {
|
||||
try {
|
||||
// 1. Fetch all categories
|
||||
const categories = await prisma.category.findMany({
|
||||
orderBy: { name: 'asc' },
|
||||
});
|
||||
|
||||
// 2. Convert array to a lookup map
|
||||
const lookup = {};
|
||||
categories.forEach(cat => {
|
||||
lookup[cat.id] = { ...cat, children: [] };
|
||||
});
|
||||
|
||||
const hierarchy = [];
|
||||
|
||||
// 3. Build hierarchical structure
|
||||
categories.forEach(cat => {
|
||||
if (cat.parentId) {
|
||||
lookup[cat.parentId].children.push(lookup[cat.id]);
|
||||
} else {
|
||||
hierarchy.push(lookup[cat.id]);
|
||||
}
|
||||
});
|
||||
|
||||
res.status(200).json({
|
||||
// statusCode: 200,
|
||||
status: true,
|
||||
message: 'Category hierarchy fetched successfully',
|
||||
data: hierarchy,
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
exports.getCategoryById = async (req, res) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
|
||||
if (!id) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: 'Category ID is required',
|
||||
});
|
||||
}
|
||||
|
||||
const category = await prisma.category.findUnique({
|
||||
where: {
|
||||
id: id, // ✅ PASS THE ACTUAL STRING VALUE
|
||||
},
|
||||
});
|
||||
|
||||
if (!category) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: 'Category not found',
|
||||
});
|
||||
}
|
||||
|
||||
return res.status(200).json({
|
||||
success: true,
|
||||
message: 'Category details fetched successfully',
|
||||
data: category,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Get category by id error:', error);
|
||||
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: 'Error fetching category',
|
||||
});
|
||||
}
|
||||
};
|
||||
463
src/controllers/admin/couponController.js
Normal file
463
src/controllers/admin/couponController.js
Normal file
@@ -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);
|
||||
}
|
||||
};
|
||||
589
src/controllers/admin/dashboardController.js
Normal file
589
src/controllers/admin/dashboardController.js
Normal file
@@ -0,0 +1,589 @@
|
||||
const { prisma } = require('../../config/database');
|
||||
const Product = require('../../models/mongodb/Product');
|
||||
const {
|
||||
getLowStockProducts,
|
||||
getInventoryStats,
|
||||
} = require('../../services/inventoryService');
|
||||
|
||||
/**
|
||||
* @desc Get dashboard stats with order overview graphs
|
||||
* @route GET /api/admin/dashboard/stats
|
||||
* @access Private/Admin
|
||||
*/
|
||||
|
||||
exports.getDashboardStats = async (req, res, next) => {
|
||||
try {
|
||||
const [
|
||||
totalUsers,
|
||||
totalOrders,
|
||||
totalProducts,
|
||||
totalRevenue,
|
||||
recentOrders,
|
||||
topSellingProducts,
|
||||
lowStockProducts,
|
||||
inventoryStats,
|
||||
orderOverview,
|
||||
revenueOverview,
|
||||
ordersByStatus,
|
||||
monthlyComparison,
|
||||
] = await Promise.all([
|
||||
prisma.user.count(),
|
||||
prisma.order.count(),
|
||||
Product.countDocuments({ status: 'active' }),
|
||||
prisma.order.aggregate({
|
||||
_sum: { totalAmount: true },
|
||||
where: { paymentStatus: 'PAID' },
|
||||
}),
|
||||
prisma.order.findMany({
|
||||
take: 5,
|
||||
orderBy: { createdAt: 'desc' },
|
||||
include: {
|
||||
user: {
|
||||
select: { id: true, email: true, firstName: true, lastName: true },
|
||||
},
|
||||
items: true,
|
||||
},
|
||||
}),
|
||||
getTopSellingProducts(), // ✅ Real top sellers
|
||||
getLowStockProducts(10), // ✅ Low stock alerts
|
||||
getInventoryStats(), // ✅ Inventory overview
|
||||
getOrderOverview(),
|
||||
getRevenueOverview(),
|
||||
getOrdersByStatus(),
|
||||
getMonthlyComparison(),
|
||||
]);
|
||||
|
||||
// Enhance recent orders with product images
|
||||
const orderProductIds = recentOrders
|
||||
.flatMap(order => order.items.map(item => item.productId))
|
||||
.filter(Boolean);
|
||||
|
||||
const orderProducts = await Product.find({
|
||||
_id: { $in: orderProductIds },
|
||||
}).lean();
|
||||
|
||||
const productMap = {};
|
||||
orderProducts.forEach(product => {
|
||||
productMap[product._id.toString()] = {
|
||||
...product,
|
||||
displayImage: getProductImage(product),
|
||||
};
|
||||
});
|
||||
|
||||
const enhancedRecentOrders = recentOrders.map(order => ({
|
||||
...order,
|
||||
items: order.items.map(item => ({
|
||||
...item,
|
||||
productImage:
|
||||
productMap[item.productId]?.displayImage ||
|
||||
'https://via.placeholder.com/300',
|
||||
productDetails: productMap[item.productId] || null,
|
||||
})),
|
||||
}));
|
||||
|
||||
res.status(200).json({
|
||||
statusCode: 200,
|
||||
status: true,
|
||||
message: 'Dashboard stats fetched successfully',
|
||||
data: {
|
||||
// Summary Stats
|
||||
totalUsers,
|
||||
totalOrders,
|
||||
totalProducts,
|
||||
totalRevenue: parseFloat(totalRevenue._sum.totalAmount || 0),
|
||||
|
||||
// ✅ Inventory Stats
|
||||
inventory: inventoryStats,
|
||||
|
||||
// Lists
|
||||
recentOrders: enhancedRecentOrders,
|
||||
topProducts: topSellingProducts, // ✅ Real selling data
|
||||
lowStockProducts, // ✅ Products needing restock
|
||||
|
||||
// Charts
|
||||
charts: {
|
||||
orderOverview,
|
||||
revenueOverview,
|
||||
ordersByStatus,
|
||||
monthlyComparison,
|
||||
},
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Dashboard stats error:', error);
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
// ✅ GET REAL TOP SELLING PRODUCTS
|
||||
async function getTopSellingProducts() {
|
||||
try {
|
||||
// Get sales data from order items
|
||||
const salesData = await prisma.orderItem.groupBy({
|
||||
by: ['productId'],
|
||||
_sum: { quantity: true },
|
||||
_count: { productId: true },
|
||||
orderBy: { _sum: { quantity: 'desc' } },
|
||||
take: 10,
|
||||
});
|
||||
|
||||
if (salesData.length === 0) {
|
||||
// Fallback: Return recent products
|
||||
const fallback = await Product.find({ status: 'active' })
|
||||
.sort({ createdAt: -1 })
|
||||
.limit(5)
|
||||
.lean();
|
||||
|
||||
return fallback.map(p => ({
|
||||
...p,
|
||||
_id: p._id.toString(),
|
||||
displayImage: getProductImage(p),
|
||||
totalSold: 0,
|
||||
totalOrders: 0,
|
||||
stock: p.stock || 0,
|
||||
stockStatus: getStockStatus(p.stock || 0),
|
||||
}));
|
||||
}
|
||||
|
||||
const productIds = salesData.map(item => item.productId);
|
||||
const products = await Product.find({
|
||||
_id: { $in: productIds },
|
||||
status: 'active',
|
||||
}).lean();
|
||||
|
||||
const statsMap = {};
|
||||
salesData.forEach(item => {
|
||||
statsMap[item.productId] = {
|
||||
totalSold: item._sum.quantity || 0,
|
||||
totalOrders: item._count.productId || 0,
|
||||
};
|
||||
});
|
||||
|
||||
function calculateStock(product) {
|
||||
if (product.variants?.length > 0) {
|
||||
return product.variants
|
||||
.filter(v => v.isActive)
|
||||
.reduce((sum, v) => sum + (v.inventory?.quantity || 0), 0);
|
||||
}
|
||||
return product.stock || 0;
|
||||
}
|
||||
|
||||
const topProducts = products
|
||||
.map(product => {
|
||||
const stats = statsMap[product._id.toString()] || {
|
||||
totalSold: 0,
|
||||
totalOrders: 0,
|
||||
};
|
||||
|
||||
const stock =
|
||||
product.variants?.length > 0
|
||||
? product.variants
|
||||
.filter(v => v.isActive)
|
||||
.reduce((sum, v) => sum + (v.inventory?.quantity || 0), 0)
|
||||
: product.stock || 0;
|
||||
|
||||
return {
|
||||
_id: product._id.toString(),
|
||||
name: product.name,
|
||||
slug: product.slug,
|
||||
basePrice: product.basePrice,
|
||||
displayImage: getProductImage(product),
|
||||
totalSold: stats.totalSold,
|
||||
totalOrders: stats.totalOrders,
|
||||
revenue: stats.totalSold * (product.basePrice || 0),
|
||||
stock,
|
||||
stockStatus: getStockStatus(stock),
|
||||
};
|
||||
})
|
||||
.sort((a, b) => b.totalSold - a.totalSold)
|
||||
.slice(0, 5);
|
||||
|
||||
return topProducts;
|
||||
} catch (error) {
|
||||
console.error('Error fetching top selling products:', error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
function getStockStatus(stock) {
|
||||
if (stock === 0) return 'OUT_OF_STOCK';
|
||||
if (stock <= 5) return 'CRITICAL';
|
||||
if (stock <= 10) return 'LOW';
|
||||
return 'IN_STOCK';
|
||||
}
|
||||
|
||||
function getProductImage(product) {
|
||||
return (
|
||||
product.images?.gallery?.[0] ||
|
||||
product.images?.primary ||
|
||||
product.variants?.[0]?.images?.[0] ||
|
||||
'https://via.placeholder.com/300'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get daily order count for last 30 days
|
||||
*/
|
||||
// async function getOrderOverview() {
|
||||
// const thirtyDaysAgo = new Date();
|
||||
// thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30);
|
||||
|
||||
// // Group orders by date
|
||||
// const orders = await prisma.order.findMany({
|
||||
// where: {
|
||||
// createdAt: {
|
||||
// gte: thirtyDaysAgo,
|
||||
// },
|
||||
// },
|
||||
// select: {
|
||||
// createdAt: true,
|
||||
// status: true,
|
||||
// },
|
||||
// });
|
||||
|
||||
// // Create date map for last 30 days
|
||||
// const dateMap = {};
|
||||
// for (let i = 29; i >= 0; i--) {
|
||||
// const date = new Date();
|
||||
// date.setDate(date.getDate() - i);
|
||||
// const dateKey = date.toISOString().split('T')[0];
|
||||
// dateMap[dateKey] = { total: 0, completed: 0, pending: 0, cancelled: 0 };
|
||||
// }
|
||||
|
||||
// // Count orders by date
|
||||
// orders.forEach(order => {
|
||||
// const dateKey = order.createdAt.toISOString().split('T')[0];
|
||||
// if (dateMap[dateKey]) {
|
||||
// dateMap[dateKey].total++;
|
||||
|
||||
// if (order.status === 'DELIVERED') {
|
||||
// dateMap[dateKey].completed++;
|
||||
// } else if (['PENDING', 'CONFIRMED', 'PROCESSING', 'SHIPPED'].includes(order.status)) {
|
||||
// dateMap[dateKey].pending++;
|
||||
// } else if (order.status === 'CANCELLED') {
|
||||
// dateMap[dateKey].cancelled++;
|
||||
// }
|
||||
// }
|
||||
// });
|
||||
|
||||
// // Convert to array format for charts
|
||||
// return Object.entries(dateMap).map(([date, counts]) => ({
|
||||
// date,
|
||||
// label: new Date(date).toLocaleDateString('en-IN', { month: 'short', day: 'numeric' }),
|
||||
// total: counts.total,
|
||||
// completed: counts.completed,
|
||||
// pending: counts.pending,
|
||||
// cancelled: counts.cancelled,
|
||||
// }));
|
||||
// }
|
||||
|
||||
async function getOrderOverview() {
|
||||
const thirtyDaysAgo = new Date();
|
||||
thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30);
|
||||
|
||||
const orders = await prisma.order.findMany({
|
||||
where: { createdAt: { gte: thirtyDaysAgo } },
|
||||
select: { createdAt: true, status: true },
|
||||
});
|
||||
|
||||
const dateMap = {};
|
||||
for (let i = 29; i >= 0; i--) {
|
||||
const date = new Date();
|
||||
date.setDate(date.getDate() - i);
|
||||
const dateKey = date.toISOString().split('T')[0];
|
||||
dateMap[dateKey] = { total: 0, completed: 0, pending: 0, cancelled: 0 };
|
||||
}
|
||||
|
||||
orders.forEach(order => {
|
||||
const dateKey = order.createdAt.toISOString().split('T')[0];
|
||||
if (dateMap[dateKey]) {
|
||||
dateMap[dateKey].total++;
|
||||
if (order.status === 'DELIVERED') dateMap[dateKey].completed++;
|
||||
else if (
|
||||
['PENDING', 'CONFIRMED', 'PROCESSING', 'SHIPPED'].includes(order.status)
|
||||
)
|
||||
dateMap[dateKey].pending++;
|
||||
else if (order.status === 'CANCELLED') dateMap[dateKey].cancelled++;
|
||||
}
|
||||
});
|
||||
|
||||
return Object.entries(dateMap).map(([date, counts]) => ({
|
||||
date,
|
||||
label: new Date(date).toLocaleDateString('en-IN', {
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
}),
|
||||
...counts,
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get daily revenue for last 30 days
|
||||
*/
|
||||
// async function getRevenueOverview() {
|
||||
// const thirtyDaysAgo = new Date();
|
||||
// thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30);
|
||||
|
||||
// const orders = await prisma.order.findMany({
|
||||
// where: {
|
||||
// createdAt: {
|
||||
// gte: thirtyDaysAgo,
|
||||
// },
|
||||
// paymentStatus: 'PAID',
|
||||
// },
|
||||
// select: {
|
||||
// createdAt: true,
|
||||
// totalAmount: true,
|
||||
// },
|
||||
// });
|
||||
|
||||
// // Create date map
|
||||
// const dateMap = {};
|
||||
// for (let i = 29; i >= 0; i--) {
|
||||
// const date = new Date();
|
||||
// date.setDate(date.getDate() - i);
|
||||
// const dateKey = date.toISOString().split('T')[0];
|
||||
// dateMap[dateKey] = 0;
|
||||
// }
|
||||
|
||||
// // Sum revenue by date
|
||||
// orders.forEach(order => {
|
||||
// const dateKey = order.createdAt.toISOString().split('T')[0];
|
||||
// if (dateMap[dateKey] !== undefined) {
|
||||
// dateMap[dateKey] += parseFloat(order.totalAmount);
|
||||
// }
|
||||
// });
|
||||
|
||||
// return Object.entries(dateMap).map(([date, revenue]) => ({
|
||||
// date,
|
||||
// label: new Date(date).toLocaleDateString('en-IN', { month: 'short', day: 'numeric' }),
|
||||
// revenue: Math.round(revenue * 100) / 100, // Round to 2 decimals
|
||||
// }));
|
||||
// }
|
||||
|
||||
async function getRevenueOverview() {
|
||||
const thirtyDaysAgo = new Date();
|
||||
thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30);
|
||||
|
||||
const orders = await prisma.order.findMany({
|
||||
where: { createdAt: { gte: thirtyDaysAgo }, paymentStatus: 'PAID' },
|
||||
select: { createdAt: true, totalAmount: true },
|
||||
});
|
||||
|
||||
const dateMap = {};
|
||||
for (let i = 29; i >= 0; i--) {
|
||||
const date = new Date();
|
||||
date.setDate(date.getDate() - i);
|
||||
dateMap[date.toISOString().split('T')[0]] = 0;
|
||||
}
|
||||
|
||||
orders.forEach(order => {
|
||||
const dateKey = order.createdAt.toISOString().split('T')[0];
|
||||
if (dateMap[dateKey] !== undefined) {
|
||||
dateMap[dateKey] += parseFloat(order.totalAmount);
|
||||
}
|
||||
});
|
||||
|
||||
return Object.entries(dateMap).map(([date, revenue]) => ({
|
||||
date,
|
||||
label: new Date(date).toLocaleDateString('en-IN', {
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
}),
|
||||
revenue: Math.round(revenue * 100) / 100,
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get order counts by status
|
||||
*/
|
||||
// async function getOrdersByStatus() {
|
||||
// const statusCounts = await prisma.order.groupBy({
|
||||
// by: ['status'],
|
||||
// _count: true,
|
||||
// });
|
||||
|
||||
// const statusLabels = {
|
||||
// PENDING: 'Pending',
|
||||
// CONFIRMED: 'Confirmed',
|
||||
// PROCESSING: 'Processing',
|
||||
// SHIPPED: 'Shipped',
|
||||
// DELIVERED: 'Delivered',
|
||||
// CANCELLED: 'Cancelled',
|
||||
// RETURN_REQUESTED: 'Return Requested',
|
||||
// };
|
||||
|
||||
// const statusColors = {
|
||||
// PENDING: '#FCD34D',
|
||||
// CONFIRMED: '#60A5FA',
|
||||
// PROCESSING: '#A78BFA',
|
||||
// SHIPPED: '#C084FC',
|
||||
// DELIVERED: '#34D399',
|
||||
// CANCELLED: '#F87171',
|
||||
// RETURN_REQUESTED: '#FB923C',
|
||||
// };
|
||||
|
||||
// return statusCounts.map(item => ({
|
||||
// status: item.status,
|
||||
// label: statusLabels[item.status] || item.status,
|
||||
// count: item._count,
|
||||
// color: statusColors[item.status] || '#9CA3AF',
|
||||
// }));
|
||||
// }
|
||||
|
||||
async function getOrdersByStatus() {
|
||||
const statusCounts = await prisma.order.groupBy({
|
||||
by: ['status'],
|
||||
_count: true,
|
||||
});
|
||||
|
||||
const labels = {
|
||||
PENDING: 'Pending',
|
||||
CONFIRMED: 'Confirmed',
|
||||
PROCESSING: 'Processing',
|
||||
SHIPPED: 'Shipped',
|
||||
DELIVERED: 'Delivered',
|
||||
CANCELLED: 'Cancelled',
|
||||
RETURN_REQUESTED: 'Return Requested',
|
||||
};
|
||||
|
||||
const colors = {
|
||||
PENDING: '#FCD34D',
|
||||
CONFIRMED: '#60A5FA',
|
||||
PROCESSING: '#A78BFA',
|
||||
SHIPPED: '#C084FC',
|
||||
DELIVERED: '#34D399',
|
||||
CANCELLED: '#F87171',
|
||||
RETURN_REQUESTED: '#FB923C',
|
||||
};
|
||||
|
||||
return statusCounts.map(item => ({
|
||||
status: item.status,
|
||||
label: labels[item.status] || item.status,
|
||||
count: item._count,
|
||||
color: colors[item.status] || '#9CA3AF',
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Compare current month vs previous month
|
||||
*/
|
||||
// async function getMonthlyComparison() {
|
||||
// const now = new Date();
|
||||
|
||||
// // Current month dates
|
||||
// const currentMonthStart = new Date(now.getFullYear(), now.getMonth(), 1);
|
||||
// const currentMonthEnd = new Date(now.getFullYear(), now.getMonth() + 1, 0);
|
||||
|
||||
// // Previous month dates
|
||||
// const previousMonthStart = new Date(now.getFullYear(), now.getMonth() - 1, 1);
|
||||
// const previousMonthEnd = new Date(now.getFullYear(), now.getMonth(), 0);
|
||||
|
||||
// const [currentMonth, previousMonth] = await Promise.all([
|
||||
// prisma.order.aggregate({
|
||||
// where: {
|
||||
// createdAt: {
|
||||
// gte: currentMonthStart,
|
||||
// lte: currentMonthEnd,
|
||||
// },
|
||||
// },
|
||||
// _count: true,
|
||||
// _sum: {
|
||||
// totalAmount: true,
|
||||
// },
|
||||
// }),
|
||||
// prisma.order.aggregate({
|
||||
// where: {
|
||||
// createdAt: {
|
||||
// gte: previousMonthStart,
|
||||
// lte: previousMonthEnd,
|
||||
// },
|
||||
// },
|
||||
// _count: true,
|
||||
// _sum: {
|
||||
// totalAmount: true,
|
||||
// },
|
||||
// }),
|
||||
// ]);
|
||||
|
||||
// const currentRevenue = parseFloat(currentMonth._sum.totalAmount || 0);
|
||||
// const previousRevenue = parseFloat(previousMonth._sum.totalAmount || 0);
|
||||
|
||||
// const orderGrowth = previousMonth._count > 0
|
||||
// ? ((currentMonth._count - previousMonth._count) / previousMonth._count) * 100
|
||||
// : 100;
|
||||
|
||||
// const revenueGrowth = previousRevenue > 0
|
||||
// ? ((currentRevenue - previousRevenue) / previousRevenue) * 100
|
||||
// : 100;
|
||||
|
||||
// return {
|
||||
// currentMonth: {
|
||||
// orders: currentMonth._count,
|
||||
// revenue: Math.round(currentRevenue * 100) / 100,
|
||||
// label: currentMonthStart.toLocaleDateString('en-IN', { month: 'long' }),
|
||||
// },
|
||||
// previousMonth: {
|
||||
// orders: previousMonth._count,
|
||||
// revenue: Math.round(previousRevenue * 100) / 100,
|
||||
// label: previousMonthStart.toLocaleDateString('en-IN', { month: 'long' }),
|
||||
// },
|
||||
// growth: {
|
||||
// orders: Math.round(orderGrowth * 10) / 10,
|
||||
// revenue: Math.round(revenueGrowth * 10) / 10,
|
||||
// },
|
||||
// };
|
||||
// }
|
||||
|
||||
async function getMonthlyComparison() {
|
||||
const now = new Date();
|
||||
const currentMonthStart = new Date(now.getFullYear(), now.getMonth(), 1);
|
||||
const currentMonthEnd = new Date(now.getFullYear(), now.getMonth() + 1, 0);
|
||||
const previousMonthStart = new Date(now.getFullYear(), now.getMonth() - 1, 1);
|
||||
const previousMonthEnd = new Date(now.getFullYear(), now.getMonth(), 0);
|
||||
|
||||
const [currentMonth, previousMonth] = await Promise.all([
|
||||
prisma.order.aggregate({
|
||||
where: { createdAt: { gte: currentMonthStart, lte: currentMonthEnd } },
|
||||
_count: true,
|
||||
_sum: { totalAmount: true },
|
||||
}),
|
||||
prisma.order.aggregate({
|
||||
where: { createdAt: { gte: previousMonthStart, lte: previousMonthEnd } },
|
||||
_count: true,
|
||||
_sum: { totalAmount: true },
|
||||
}),
|
||||
]);
|
||||
|
||||
const currentRevenue = parseFloat(currentMonth._sum.totalAmount || 0);
|
||||
const previousRevenue = parseFloat(previousMonth._sum.totalAmount || 0);
|
||||
|
||||
return {
|
||||
currentMonth: {
|
||||
orders: currentMonth._count,
|
||||
revenue: Math.round(currentRevenue * 100) / 100,
|
||||
label: currentMonthStart.toLocaleDateString('en-IN', { month: 'long' }),
|
||||
},
|
||||
previousMonth: {
|
||||
orders: previousMonth._count,
|
||||
revenue: Math.round(previousRevenue * 100) / 100,
|
||||
label: previousMonthStart.toLocaleDateString('en-IN', { month: 'long' }),
|
||||
},
|
||||
growth: {
|
||||
orders:
|
||||
previousMonth._count > 0
|
||||
? Math.round(
|
||||
((currentMonth._count - previousMonth._count) /
|
||||
previousMonth._count) *
|
||||
1000
|
||||
) / 10
|
||||
: 100,
|
||||
revenue:
|
||||
previousRevenue > 0
|
||||
? Math.round(
|
||||
((currentRevenue - previousRevenue) / previousRevenue) * 1000
|
||||
) / 10
|
||||
: 100,
|
||||
},
|
||||
};
|
||||
}
|
||||
395
src/controllers/admin/orderController.js
Normal file
395
src/controllers/admin/orderController.js
Normal file
@@ -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);
|
||||
}
|
||||
};
|
||||
782
src/controllers/admin/productController.js
Normal file
782
src/controllers/admin/productController.js
Normal file
@@ -0,0 +1,782 @@
|
||||
const Product = require('../../models/mongodb/Product');
|
||||
const uploadToS3 = require("../../utils/uploadToS3");
|
||||
|
||||
|
||||
// exports.getAllProducts = async (req, res, next) => {
|
||||
// try {
|
||||
// const { page = 1, limit = 20, status, category, search } = req.query;
|
||||
// const skip = (page - 1) * limit;
|
||||
|
||||
// const query = {};
|
||||
// if (status) query.status = status;
|
||||
// if (category) query.category = category;
|
||||
// if (search) query.$text = { $search: search };
|
||||
|
||||
// const [products, total] = await Promise.all([
|
||||
// Product.find(query)
|
||||
// .sort({ createdAt: -1 })
|
||||
// .skip(skip)
|
||||
// .limit(+limit)
|
||||
// .lean(),
|
||||
// Product.countDocuments(query),
|
||||
// ]);
|
||||
|
||||
// // res.json({
|
||||
// // success: true,
|
||||
// // data: { products, pagination: { page: +page, limit: +limit, total, pages: Math.ceil(total / limit) } },
|
||||
// // });
|
||||
// return res.status(200).json({
|
||||
// statusCode: 200,
|
||||
// status: true,
|
||||
// message: 'Products fetched successfully',
|
||||
// data: {
|
||||
// products,
|
||||
// pagination: {
|
||||
// page: +page,
|
||||
// limit: +limit,
|
||||
// total,
|
||||
// pages: Math.ceil(total / limit),
|
||||
// },
|
||||
// },
|
||||
// });
|
||||
// } catch (error) {
|
||||
// next(error);
|
||||
// }
|
||||
// };
|
||||
|
||||
// exports.createProduct = async (req, res, next) => {
|
||||
// try {
|
||||
// const product = new Product(req.body);
|
||||
// await product.save();
|
||||
// res.status(201).json({ success: true, message: 'Product created', data: product });
|
||||
// } catch (error) {
|
||||
// next(error);
|
||||
// }
|
||||
// };
|
||||
|
||||
// exports.createProduct = async (req, res, next) => {
|
||||
// try {
|
||||
// let { name, slug, ...rest } = req.body;
|
||||
|
||||
// // Generate slug from name if not provided
|
||||
// if (!slug) {
|
||||
// slug = name
|
||||
// .toLowerCase()
|
||||
// .replace(/[^a-z0-9]+/g, '-') // replace spaces & special chars
|
||||
// .replace(/(^-|-$)/g, ''); // remove leading/trailing hyphens
|
||||
// }
|
||||
|
||||
// // Ensure slug is unique
|
||||
// let slugExists = await Product.findOne({ slug });
|
||||
// let counter = 1;
|
||||
// const baseSlug = slug;
|
||||
// while (slugExists) {
|
||||
// slug = `${baseSlug}-${counter}`;
|
||||
// slugExists = await Product.findOne({ slug });
|
||||
// counter++;
|
||||
// }
|
||||
|
||||
// // ✅ Upload images manually
|
||||
// const primaryImage = req.files?.primaryImage
|
||||
// ? await uploadToS3(req.files.primaryImage[0])
|
||||
// : null;
|
||||
|
||||
// const galleryImages = req.files?.galleryImages
|
||||
// ? await Promise.all(
|
||||
// req.files.galleryImages.map((file) => uploadToS3(file))
|
||||
// )
|
||||
// : [];
|
||||
|
||||
// // const product = new Product({ name, slug, ...rest });
|
||||
// const product = new Product({
|
||||
// name,
|
||||
// slug,
|
||||
// ...rest,
|
||||
// images: {
|
||||
// primary: primaryImage,
|
||||
// gallery: galleryImages,
|
||||
// },
|
||||
// });
|
||||
// await product.save();
|
||||
|
||||
// // res.status(201).json({ success: true, message: 'Product created', data: product });
|
||||
// return res.status(201).json({
|
||||
// // statusCode: 201,
|
||||
// status: true,
|
||||
// message: 'Product created successfully',
|
||||
// data: product,
|
||||
// });
|
||||
// } catch (error) {
|
||||
// next(error);
|
||||
// }
|
||||
// };
|
||||
|
||||
|
||||
|
||||
// exports.createProduct = async (req, res, next) => {
|
||||
// try {
|
||||
// let { name, hasVariants, variants, ...rest } = req.body;
|
||||
// let slug;
|
||||
// // slug generation
|
||||
// if (!slug) {
|
||||
// slug = name
|
||||
// .toLowerCase()
|
||||
// .replace(/[^a-z0-9]+/g, "-")
|
||||
// .replace(/(^-|-$)/g, "");
|
||||
// }
|
||||
|
||||
// let exists = await Product.findOne({ slug });
|
||||
// let i = 1;
|
||||
// while (exists) {
|
||||
// slug = `${slug}-${i++}`;
|
||||
// exists = await Product.findOne({ slug });
|
||||
// }
|
||||
|
||||
// // Upload main images
|
||||
// const primaryImage = req.files?.primaryImage
|
||||
// ? await uploadToS3(req.files.primaryImage[0])
|
||||
// : null;
|
||||
|
||||
// const galleryImages = req.files?.galleryImages
|
||||
// ? await Promise.all(
|
||||
// req.files.galleryImages.map((f) => uploadToS3(f))
|
||||
// )
|
||||
// : [];
|
||||
|
||||
// const productData = {
|
||||
// name,
|
||||
// slug,
|
||||
// ...rest,
|
||||
// hasVariants: hasVariants === "true",
|
||||
// };
|
||||
|
||||
// // 🔥 VARIANT LOGIC
|
||||
// if (hasVariants === "true") {
|
||||
// const parsedVariants = JSON.parse(variants); // IMPORTANT
|
||||
|
||||
// productData.variants = await Promise.all(
|
||||
// parsedVariants.map(async (variant) => {
|
||||
// const variantImages =
|
||||
// req.files?.[`variantImages_${variant.color}`]
|
||||
// ? await Promise.all(
|
||||
// req.files[`variantImages_${variant.color}`].map(uploadToS3)
|
||||
// )
|
||||
// : [];
|
||||
|
||||
// return {
|
||||
// size: variant.size,
|
||||
// color: variant.color,
|
||||
// sku: variant.sku,
|
||||
// price: Number(variant.price),
|
||||
// compareAtPrice: Number(variant.compareAtPrice),
|
||||
// inventory: {
|
||||
// quantity: Number(variant.quantity),
|
||||
// trackInventory: true,
|
||||
// },
|
||||
// images: variantImages,
|
||||
// };
|
||||
// })
|
||||
// );
|
||||
// } else {
|
||||
// // simple product images
|
||||
// productData.images = {
|
||||
// primary: primaryImage,
|
||||
// gallery: galleryImages,
|
||||
// };
|
||||
// }
|
||||
|
||||
// const product = await Product.create(productData);
|
||||
|
||||
// res.status(201).json({
|
||||
// status: true,
|
||||
// message: "Product created successfully",
|
||||
// data: product,
|
||||
// });
|
||||
// } catch (err) {
|
||||
// next(err);
|
||||
// }
|
||||
// };
|
||||
|
||||
|
||||
// exports.createProduct = async (req, res, next) => {
|
||||
// try {
|
||||
// const { name, hasVariants, variants, ...rest } = req.body;
|
||||
|
||||
// // ✅ Validate name
|
||||
// if (!name || name.trim() === "") {
|
||||
// return res.status(400).json({
|
||||
// status: false,
|
||||
// message: "Product name is required",
|
||||
// });
|
||||
// }
|
||||
|
||||
// // slug generation
|
||||
// let slug = name
|
||||
// .toLowerCase()
|
||||
// .replace(/[^a-z0-9]+/g, "-")
|
||||
// .replace(/(^-|-$)/g, "");
|
||||
|
||||
// // ensure unique slug
|
||||
// let exists = await Product.findOne({ slug });
|
||||
// let i = 1;
|
||||
// while (exists) {
|
||||
// slug = `${slug}-${i++}`;
|
||||
// exists = await Product.findOne({ slug });
|
||||
// }
|
||||
|
||||
// // Upload main images
|
||||
// const primaryImage = req.files?.primaryImage
|
||||
// ? await uploadToS3(req.files.primaryImage[0])
|
||||
// : null;
|
||||
|
||||
// const galleryImages = req.files?.galleryImages
|
||||
// ? await Promise.all(req.files.galleryImages.map(uploadToS3))
|
||||
// : [];
|
||||
|
||||
// const productData = {
|
||||
// name,
|
||||
// slug,
|
||||
// ...rest,
|
||||
// hasVariants: hasVariants === "true",
|
||||
// };
|
||||
|
||||
// // 🔥 VARIANT LOGIC
|
||||
// if (hasVariants === "true") {
|
||||
// const parsedVariants = JSON.parse(variants); // IMPORTANT
|
||||
|
||||
// productData.variants = await Promise.all(
|
||||
// parsedVariants.map(async (variant) => {
|
||||
// const variantImages =
|
||||
// req.files?.[`variantImages_${variant.color}`]
|
||||
// ? await Promise.all(
|
||||
// req.files[`variantImages_${variant.color}`].map(uploadToS3)
|
||||
// )
|
||||
// : [];
|
||||
|
||||
// return {
|
||||
// size: variant.size,
|
||||
// color: variant.color,
|
||||
// sku: variant.sku,
|
||||
// price: Number(variant.price),
|
||||
// compareAtPrice: Number(variant.compareAtPrice),
|
||||
// inventory: {
|
||||
// quantity: Number(variant.quantity),
|
||||
// trackInventory: true,
|
||||
// },
|
||||
// images: variantImages,
|
||||
// };
|
||||
// })
|
||||
// );
|
||||
// } else {
|
||||
// // simple product images
|
||||
// productData.images = {
|
||||
// primary: primaryImage,
|
||||
// gallery: galleryImages,
|
||||
// };
|
||||
// }
|
||||
|
||||
// const product = await Product.create(productData);
|
||||
|
||||
// res.status(201).json({
|
||||
// status: true,
|
||||
// message: "Product created successfully",
|
||||
// data: product,
|
||||
// });
|
||||
// } catch (err) {
|
||||
// next(err);
|
||||
// }
|
||||
// };
|
||||
|
||||
|
||||
|
||||
// exports.createProduct = async (req, res, next) => {
|
||||
// try {
|
||||
// const { name, hasVariants, variants, ...rest } = req.body;
|
||||
|
||||
// console.log('📥 Request body:', { name, hasVariants, variantsCount: variants ? 'YES' : 'NO' });
|
||||
// console.log('📥 Files received:', req.files ? req.files.length : 0);
|
||||
|
||||
// // ✅ Validate name
|
||||
// if (!name || name.trim() === "") {
|
||||
// return res.status(400).json({
|
||||
// status: false,
|
||||
// message: "Product name is required",
|
||||
// });
|
||||
// }
|
||||
|
||||
// // Generate unique slug
|
||||
// let slug = name
|
||||
// .toLowerCase()
|
||||
// .replace(/[^a-z0-9]+/g, "-")
|
||||
// .replace(/(^-|-$)/g, "");
|
||||
|
||||
// let exists = await Product.findOne({ slug });
|
||||
// let i = 1;
|
||||
// while (exists) {
|
||||
// slug = `${slug}-${i++}`;
|
||||
// exists = await Product.findOne({ slug });
|
||||
// }
|
||||
|
||||
// const productData = {
|
||||
// name,
|
||||
// slug,
|
||||
// ...rest,
|
||||
// hasVariants: hasVariants === "true" || hasVariants === true,
|
||||
// };
|
||||
|
||||
// // ======================
|
||||
// // VARIANT MODE
|
||||
// // ======================
|
||||
// if (productData.hasVariants) {
|
||||
// console.log('🎨 Processing variant product...');
|
||||
|
||||
// if (!variants) {
|
||||
// return res.status(400).json({
|
||||
// status: false,
|
||||
// message: "Variants data is required when hasVariants is true",
|
||||
// });
|
||||
// }
|
||||
|
||||
// const parsedVariants = JSON.parse(variants);
|
||||
// console.log('📦 Parsed variants:', parsedVariants.length);
|
||||
|
||||
// // ✅ Convert req.files array to object grouped by fieldname
|
||||
// const filesGrouped = {};
|
||||
// if (req.files && req.files.length > 0) {
|
||||
// req.files.forEach(file => {
|
||||
// if (!filesGrouped[file.fieldname]) {
|
||||
// filesGrouped[file.fieldname] = [];
|
||||
// }
|
||||
// filesGrouped[file.fieldname].push(file);
|
||||
// });
|
||||
// }
|
||||
|
||||
// console.log('📸 Files grouped:', Object.keys(filesGrouped));
|
||||
|
||||
// // Process each variant
|
||||
// productData.variants = await Promise.all(
|
||||
// parsedVariants.map(async (variant) => {
|
||||
// const color = variant.color;
|
||||
// const fieldName = `variantImages_${color}`;
|
||||
|
||||
// console.log(`🔍 Looking for images with fieldname: ${fieldName}`);
|
||||
|
||||
// // Get images for this variant
|
||||
// const variantFiles = filesGrouped[fieldName] || [];
|
||||
// console.log(`📸 Found ${variantFiles.length} images for ${color}`);
|
||||
|
||||
// // Upload images to S3
|
||||
// const variantImages = variantFiles.length > 0
|
||||
// ? await Promise.all(variantFiles.map(uploadToS3))
|
||||
// : [];
|
||||
|
||||
// console.log(`✅ Uploaded ${variantImages.length} images for ${color}`);
|
||||
|
||||
// return {
|
||||
// size: variant.size || 'default',
|
||||
// color: variant.color,
|
||||
// sku: variant.sku,
|
||||
// price: Number(variant.price),
|
||||
// compareAtPrice: variant.compareAtPrice ? Number(variant.compareAtPrice) : null,
|
||||
// inventory: {
|
||||
// quantity: Number(variant.quantity || variant.stock || 0),
|
||||
// trackInventory: true,
|
||||
// },
|
||||
// images: variantImages,
|
||||
// isActive: true,
|
||||
// };
|
||||
// })
|
||||
// );
|
||||
|
||||
// console.log('✅ All variants processed:', productData.variants.length);
|
||||
// }
|
||||
// // ======================
|
||||
// // SIMPLE PRODUCT MODE
|
||||
// // ======================
|
||||
// else {
|
||||
// console.log('📦 Processing simple product...');
|
||||
|
||||
// // ✅ Handle files from req.files array
|
||||
// let primaryImage = null;
|
||||
// let galleryImages = [];
|
||||
|
||||
// if (req.files && req.files.length > 0) {
|
||||
// // Group files by fieldname
|
||||
// const filesGrouped = {};
|
||||
// req.files.forEach(file => {
|
||||
// if (!filesGrouped[file.fieldname]) {
|
||||
// filesGrouped[file.fieldname] = [];
|
||||
// }
|
||||
// filesGrouped[file.fieldname].push(file);
|
||||
// });
|
||||
|
||||
// // Upload primary image
|
||||
// if (filesGrouped['primaryImage'] && filesGrouped['primaryImage'][0]) {
|
||||
// primaryImage = await uploadToS3(filesGrouped['primaryImage'][0]);
|
||||
// }
|
||||
|
||||
// // Upload gallery images
|
||||
// if (filesGrouped['galleryImages']) {
|
||||
// galleryImages = await Promise.all(
|
||||
// filesGrouped['galleryImages'].map(uploadToS3)
|
||||
// );
|
||||
// }
|
||||
// }
|
||||
|
||||
// productData.images = {
|
||||
// primary: primaryImage,
|
||||
// gallery: galleryImages,
|
||||
// videos: [],
|
||||
// };
|
||||
|
||||
// console.log('✅ Images uploaded:', {
|
||||
// primary: !!primaryImage,
|
||||
// gallery: galleryImages.length,
|
||||
// });
|
||||
// }
|
||||
|
||||
// // Create product in MongoDB
|
||||
// const product = await Product.create(productData);
|
||||
|
||||
// console.log('✅ Product created:', product._id);
|
||||
|
||||
// res.status(201).json({
|
||||
// status: true,
|
||||
// message: "Product created successfully",
|
||||
// data: product,
|
||||
// });
|
||||
// } catch (err) {
|
||||
// console.error('❌ Error creating product:', err);
|
||||
|
||||
// // Send detailed error for debugging
|
||||
// if (process.env.NODE_ENV === 'development') {
|
||||
// return res.status(400).json({
|
||||
// status: false,
|
||||
// message: "Failed to create product",
|
||||
// error: err.message,
|
||||
// stack: err.stack,
|
||||
// });
|
||||
// }
|
||||
|
||||
// next(err);
|
||||
// }
|
||||
// };
|
||||
|
||||
|
||||
exports.getAllProducts = async (req, res, next) => {
|
||||
try {
|
||||
const { page = 1, limit = 20, status, category, search } = req.query;
|
||||
const skip = (page - 1) * limit;
|
||||
|
||||
const query = {};
|
||||
if (status) query.status = status;
|
||||
if (category) query.category = category;
|
||||
if (search) query.$text = { $search: search };
|
||||
|
||||
const [products, total] = await Promise.all([
|
||||
Product.find(query)
|
||||
.sort({ createdAt: -1 })
|
||||
.skip(skip)
|
||||
.limit(+limit)
|
||||
.lean(),
|
||||
Product.countDocuments(query),
|
||||
]);
|
||||
|
||||
// ✅ Compute real-time stock for each product
|
||||
const productsWithStock = products.map(product => {
|
||||
const stockInfo = computeStockInfo(product);
|
||||
return {
|
||||
...product,
|
||||
...stockInfo,
|
||||
};
|
||||
});
|
||||
|
||||
return res.status(200).json({
|
||||
statusCode: 200,
|
||||
status: true,
|
||||
message: 'Products fetched successfully',
|
||||
data: {
|
||||
products: productsWithStock,
|
||||
pagination: {
|
||||
page: +page,
|
||||
limit: +limit,
|
||||
total,
|
||||
pages: Math.ceil(total / limit),
|
||||
},
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
// ✅ Central stock calculator — use this everywhere
|
||||
function computeStockInfo(product) {
|
||||
let totalStock = 0;
|
||||
let variantStockDetails = [];
|
||||
|
||||
if (product.hasVariants && product.variants?.length > 0) {
|
||||
// Sum stock across all active variants
|
||||
variantStockDetails = product.variants
|
||||
.filter(v => v.isActive)
|
||||
.map(v => ({
|
||||
variantId: v._id?.toString(),
|
||||
sku: v.sku,
|
||||
size: v.size,
|
||||
color: v.color,
|
||||
price: v.price,
|
||||
stock: v.inventory?.quantity || 0,
|
||||
trackInventory: v.inventory?.trackInventory ?? true,
|
||||
stockStatus: getStockStatus(v.inventory?.quantity || 0),
|
||||
}));
|
||||
|
||||
totalStock = variantStockDetails.reduce((sum, v) => sum + v.stock, 0);
|
||||
} else {
|
||||
// Non-variant product — use root stock field
|
||||
totalStock = product.stock || 0;
|
||||
variantStockDetails = [];
|
||||
}
|
||||
|
||||
return {
|
||||
totalStock,
|
||||
stockStatus: getStockStatus(totalStock),
|
||||
variantStock: variantStockDetails, // per-variant breakdown
|
||||
};
|
||||
}
|
||||
|
||||
function getStockStatus(stock) {
|
||||
if (stock === 0) return 'OUT_OF_STOCK';
|
||||
if (stock <= 5) return 'CRITICAL';
|
||||
if (stock <= 10) return 'LOW';
|
||||
return 'IN_STOCK';
|
||||
}
|
||||
|
||||
|
||||
exports.createProduct = async (req, res, next) => {
|
||||
try {
|
||||
const { name, hasVariants, variants, ...rest } = req.body;
|
||||
|
||||
console.log('📥 Request body:', { name, hasVariants, variantsCount: variants ? 'YES' : 'NO' });
|
||||
console.log('📥 Files received:', req.files ? req.files.length : 0);
|
||||
|
||||
// ✅ Validate name
|
||||
if (!name || name.trim() === "") {
|
||||
return res.status(400).json({
|
||||
status: false,
|
||||
message: "Product name is required",
|
||||
});
|
||||
}
|
||||
|
||||
// Generate unique slug
|
||||
let slug = name
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9]+/g, "-")
|
||||
.replace(/(^-|-$)/g, "");
|
||||
|
||||
let exists = await Product.findOne({ slug });
|
||||
let i = 1;
|
||||
while (exists) {
|
||||
slug = `${slug}-${i++}`;
|
||||
exists = await Product.findOne({ slug });
|
||||
}
|
||||
|
||||
// ✅ Build productData carefully to avoid empty slug
|
||||
const productData = {
|
||||
name,
|
||||
slug, // Generated slug
|
||||
hasVariants: hasVariants === "true" || hasVariants === true,
|
||||
};
|
||||
|
||||
// ✅ Add other fields from rest, but skip 'slug' if it exists
|
||||
Object.keys(rest).forEach(key => {
|
||||
if (key !== 'slug') {
|
||||
productData[key] = rest[key];
|
||||
}
|
||||
});
|
||||
|
||||
// ======================
|
||||
// VARIANT MODE
|
||||
// ======================
|
||||
if (productData.hasVariants) {
|
||||
console.log('🎨 Processing variant product...');
|
||||
|
||||
if (!variants) {
|
||||
return res.status(400).json({
|
||||
status: false,
|
||||
message: "Variants data is required when hasVariants is true",
|
||||
});
|
||||
}
|
||||
|
||||
const parsedVariants = JSON.parse(variants);
|
||||
console.log('📦 Parsed variants:', parsedVariants.length);
|
||||
|
||||
// ✅ Convert req.files array to object grouped by fieldname
|
||||
const filesGrouped = {};
|
||||
if (req.files && req.files.length > 0) {
|
||||
req.files.forEach(file => {
|
||||
if (!filesGrouped[file.fieldname]) {
|
||||
filesGrouped[file.fieldname] = [];
|
||||
}
|
||||
filesGrouped[file.fieldname].push(file);
|
||||
});
|
||||
}
|
||||
|
||||
console.log('📸 Files grouped:', Object.keys(filesGrouped));
|
||||
|
||||
// Process each variant
|
||||
productData.variants = await Promise.all(
|
||||
parsedVariants.map(async (variant) => {
|
||||
const color = variant.color;
|
||||
const fieldName = `variantImages_${color}`;
|
||||
|
||||
console.log(`🔍 Looking for images with fieldname: ${fieldName}`);
|
||||
|
||||
// Get images for this variant
|
||||
const variantFiles = filesGrouped[fieldName] || [];
|
||||
console.log(`📸 Found ${variantFiles.length} images for ${color}`);
|
||||
|
||||
// Upload images to S3
|
||||
const variantImages = variantFiles.length > 0
|
||||
? await Promise.all(variantFiles.map(uploadToS3))
|
||||
: [];
|
||||
|
||||
console.log(`✅ Uploaded ${variantImages.length} images for ${color}`);
|
||||
|
||||
return {
|
||||
size: variant.size || 'default',
|
||||
color: variant.color,
|
||||
sku: variant.sku,
|
||||
price: Number(variant.price),
|
||||
compareAtPrice: variant.compareAtPrice ? Number(variant.compareAtPrice) : null,
|
||||
inventory: {
|
||||
quantity: Number(variant.quantity || variant.stock || 0),
|
||||
trackInventory: true,
|
||||
},
|
||||
images: variantImages,
|
||||
isActive: true,
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
console.log('✅ All variants processed:', productData.variants.length);
|
||||
}
|
||||
// ======================
|
||||
// SIMPLE PRODUCT MODE
|
||||
// ======================
|
||||
else {
|
||||
console.log('📦 Processing simple product...');
|
||||
|
||||
// ✅ Handle files from req.files array
|
||||
let primaryImage = null;
|
||||
let galleryImages = [];
|
||||
|
||||
if (req.files && req.files.length > 0) {
|
||||
// Group files by fieldname
|
||||
const filesGrouped = {};
|
||||
req.files.forEach(file => {
|
||||
if (!filesGrouped[file.fieldname]) {
|
||||
filesGrouped[file.fieldname] = [];
|
||||
}
|
||||
filesGrouped[file.fieldname].push(file);
|
||||
});
|
||||
|
||||
// Upload primary image
|
||||
if (filesGrouped['primaryImage'] && filesGrouped['primaryImage'][0]) {
|
||||
primaryImage = await uploadToS3(filesGrouped['primaryImage'][0]);
|
||||
}
|
||||
|
||||
// Upload gallery images
|
||||
if (filesGrouped['galleryImages']) {
|
||||
galleryImages = await Promise.all(
|
||||
filesGrouped['galleryImages'].map(uploadToS3)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
productData.images = {
|
||||
primary: primaryImage,
|
||||
gallery: galleryImages,
|
||||
videos: [],
|
||||
};
|
||||
|
||||
console.log('✅ Images uploaded:', {
|
||||
primary: !!primaryImage,
|
||||
gallery: galleryImages.length,
|
||||
});
|
||||
}
|
||||
|
||||
// Create product in MongoDB
|
||||
const product = await Product.create(productData);
|
||||
|
||||
console.log('✅ Product created:', product._id);
|
||||
|
||||
res.status(201).json({
|
||||
status: true,
|
||||
message: "Product created successfully",
|
||||
data: product,
|
||||
});
|
||||
} catch (err) {
|
||||
console.error('❌ Error creating product:', err);
|
||||
|
||||
// Send detailed error for debugging
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
return res.status(400).json({
|
||||
status: false,
|
||||
message: "Failed to create product",
|
||||
error: err.message,
|
||||
stack: err.stack,
|
||||
});
|
||||
}
|
||||
|
||||
next(err);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
exports.updateProduct = async (req, res, next) => {
|
||||
try {
|
||||
const product = await Product.findByIdAndUpdate(req.params.id, req.body, {
|
||||
new: true,
|
||||
});
|
||||
// if (!product) return res.status(404).json({ success: false, message: 'Product not found' });
|
||||
if (!product) {
|
||||
return res.status(404).json({
|
||||
statusCode: 404,
|
||||
status: false,
|
||||
message: 'Product not found',
|
||||
});
|
||||
}
|
||||
// res.json({ success: true, message: 'Product updated', data: product });
|
||||
return res.status(200).json({
|
||||
statusCode: 200,
|
||||
status: true,
|
||||
message: 'Product updated successfully',
|
||||
data: product,
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
exports.deleteProduct = async (req, res, next) => {
|
||||
try {
|
||||
const product = await Product.findByIdAndDelete(req.params.id);
|
||||
// if (!product) return res.status(404).json({ success: false, message: 'Product not found' });
|
||||
if (!product) {
|
||||
return res.status(404).json({
|
||||
statusCode: 404,
|
||||
status: false,
|
||||
message: 'Product not found',
|
||||
});
|
||||
}
|
||||
// res.json({ success: true, message: 'Product deleted' });
|
||||
return res.status(200).json({
|
||||
statusCode: 200,
|
||||
status: true,
|
||||
message: 'Product deleted successfully',
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
1170
src/controllers/admin/reportController.js
Normal file
1170
src/controllers/admin/reportController.js
Normal file
File diff suppressed because it is too large
Load Diff
160
src/controllers/admin/userController.js
Normal file
160
src/controllers/admin/userController.js
Normal file
@@ -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);
|
||||
}
|
||||
};
|
||||
421
src/controllers/authController.js
Normal file
421
src/controllers/authController.js
Normal file
@@ -0,0 +1,421 @@
|
||||
const authService = require('../services/authService');
|
||||
const { generateOTP, sendOTP } = require('../services/wappconnectService');
|
||||
const { saveOTP, verifyOTP } = require('../services/otpStore');
|
||||
const { prisma } = require('../config/database');
|
||||
|
||||
// @desc Register user
|
||||
exports.register = async (req, res, next) => {
|
||||
try {
|
||||
const { email, password, firstName, lastName, username, phone, role } =
|
||||
req.body;
|
||||
|
||||
if (!email || !password) {
|
||||
return res.status(400).json({
|
||||
statusCode: 400,
|
||||
status: false,
|
||||
message: 'Email and password are required',
|
||||
});
|
||||
}
|
||||
|
||||
if (password.length < 6) {
|
||||
return res.status(400).json({
|
||||
statusCode: 400,
|
||||
status: false,
|
||||
message: 'Password must be at least 6 characters',
|
||||
});
|
||||
}
|
||||
|
||||
const result = await authService.register({
|
||||
email,
|
||||
password,
|
||||
firstName,
|
||||
lastName,
|
||||
username,
|
||||
phone,
|
||||
role,
|
||||
});
|
||||
|
||||
return res.status(201).json({
|
||||
statusCode: 201,
|
||||
status: true,
|
||||
message: 'User registered successfully',
|
||||
data: result,
|
||||
});
|
||||
} catch (error) {
|
||||
return res.status(500).json({
|
||||
statusCode: 500,
|
||||
status: false,
|
||||
message: error.message || 'Internal server error',
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// @desc Login user
|
||||
exports.login = async (req, res, next) => {
|
||||
try {
|
||||
const { email, password } = req.body;
|
||||
|
||||
if (!email || !password) {
|
||||
return res.status(400).json({
|
||||
statusCode: 400,
|
||||
status: false,
|
||||
message: 'Email and password are required',
|
||||
});
|
||||
}
|
||||
|
||||
const result = await authService.login(email, password);
|
||||
|
||||
return res.status(200).json({
|
||||
statusCode: 200,
|
||||
status: true,
|
||||
message: 'Login successful',
|
||||
data: result,
|
||||
});
|
||||
} catch (error) {
|
||||
return res.status(500).json({
|
||||
statusCode: 500,
|
||||
status: false,
|
||||
message: error.message || 'Internal server error',
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// ─── STEP 1: Request OTP ─────────────────────────────────────────────────────
|
||||
exports.sendLoginOTP = async (req, res, next) => {
|
||||
try {
|
||||
const { phone } = req.body;
|
||||
|
||||
if (!phone) {
|
||||
return res.status(400).json({
|
||||
status: false,
|
||||
message: 'Phone number is required',
|
||||
});
|
||||
}
|
||||
|
||||
const user = await prisma.user.findFirst({ where: { phone } });
|
||||
if (!user) {
|
||||
return res.status(404).json({
|
||||
status: false,
|
||||
message: 'No account found with this phone number',
|
||||
});
|
||||
}
|
||||
|
||||
if (!user.isActive) {
|
||||
return res.status(403).json({
|
||||
status: false,
|
||||
message: 'Account is deactivated',
|
||||
});
|
||||
}
|
||||
|
||||
const otp = generateOTP();
|
||||
saveOTP(phone, otp);
|
||||
|
||||
// ✅ Don't crash if WhatsApp fails — just log the error
|
||||
try {
|
||||
await sendOTP(phone, otp);
|
||||
} catch (whatsappError) {
|
||||
console.error('⚠️ WhatsApp send failed:', whatsappError.message);
|
||||
// Continue anyway — OTP is still saved, user can use console OTP in dev
|
||||
}
|
||||
|
||||
// Always log OTP in development
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
console.log(`\n📱 OTP for ${phone}: ${otp}`);
|
||||
console.log(`💡 Master OTP: 123456\n`);
|
||||
}
|
||||
|
||||
return res.status(200).json({
|
||||
status: true,
|
||||
message: 'OTP sent successfully',
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
// ─── STEP 2: Verify OTP & Login ──────────────────────────────────────────────
|
||||
exports.verifyLoginOTP = async (req, res, next) => {
|
||||
try {
|
||||
const { phone, otp } = req.body;
|
||||
|
||||
if (!phone || !otp) {
|
||||
return res.status(400).json({
|
||||
statusCode: 400,
|
||||
status: false,
|
||||
message: 'Phone and OTP are required',
|
||||
});
|
||||
}
|
||||
|
||||
const result = verifyOTP(phone, otp);
|
||||
if (!result.valid) {
|
||||
return res.status(400).json({
|
||||
statusCode: 400,
|
||||
status: false,
|
||||
message: result.reason,
|
||||
});
|
||||
}
|
||||
|
||||
const user = await prisma.user.findFirst({
|
||||
where: { phone },
|
||||
select: {
|
||||
id: true,
|
||||
email: true,
|
||||
firstName: true,
|
||||
lastName: true,
|
||||
phone: true,
|
||||
role: true,
|
||||
avatar: true,
|
||||
isVerified: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
return res.status(404).json({
|
||||
statusCode: 404,
|
||||
status: false,
|
||||
message: 'User not found',
|
||||
});
|
||||
}
|
||||
|
||||
await prisma.user.update({
|
||||
where: { id: user.id },
|
||||
data: { lastLoginAt: new Date() },
|
||||
});
|
||||
|
||||
// ✅ Use authService class methods directly (same as login does)
|
||||
const token = authService.generateToken({ id: user.id });
|
||||
const refreshToken = authService.generateRefreshToken({ id: user.id });
|
||||
|
||||
return res.status(200).json({
|
||||
statusCode: 200,
|
||||
status: true,
|
||||
message: 'Login successful',
|
||||
data: {
|
||||
user,
|
||||
token,
|
||||
refreshToken,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
// @desc Refresh token
|
||||
// @desc Refresh token
|
||||
exports.refreshToken = async (req, res, next) => {
|
||||
try {
|
||||
const { refreshToken } = req.body;
|
||||
|
||||
if (!refreshToken) {
|
||||
return res.status(400).json({
|
||||
statusCode: 400,
|
||||
status: false,
|
||||
message: 'Refresh token is required',
|
||||
});
|
||||
}
|
||||
|
||||
const result = await authService.refreshToken(refreshToken);
|
||||
|
||||
return res.status(200).json({
|
||||
statusCode: 200,
|
||||
status: true,
|
||||
message: 'Token refreshed successfully',
|
||||
data: result,
|
||||
});
|
||||
} catch (error) {
|
||||
return res.status(500).json({
|
||||
statusCode: 500,
|
||||
status: false,
|
||||
message: error.message || 'Internal server error',
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// @desc Logout user
|
||||
exports.logout = async (req, res, next) => {
|
||||
try {
|
||||
await authService.logout(req.user.id);
|
||||
|
||||
return res.status(200).json({
|
||||
statusCode: 200,
|
||||
status: true,
|
||||
message: 'Logout successful',
|
||||
});
|
||||
} catch (error) {
|
||||
return res.status(500).json({
|
||||
statusCode: 500,
|
||||
status: false,
|
||||
message: error.message || 'Internal server error',
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// @desc Change password
|
||||
exports.changePassword = async (req, res, next) => {
|
||||
try {
|
||||
const { currentPassword, newPassword } = req.body;
|
||||
|
||||
if (!currentPassword || !newPassword) {
|
||||
return res.status(400).json({
|
||||
statusCode: 400,
|
||||
status: false,
|
||||
message: 'Current password and new password are required',
|
||||
});
|
||||
}
|
||||
|
||||
if (newPassword.length < 6) {
|
||||
return res.status(400).json({
|
||||
statusCode: 400,
|
||||
status: false,
|
||||
message: 'New password must be at least 6 characters',
|
||||
});
|
||||
}
|
||||
|
||||
await authService.changePassword(req.user.id, currentPassword, newPassword);
|
||||
|
||||
return res.status(200).json({
|
||||
statusCode: 200,
|
||||
status: true,
|
||||
message: 'Password changed successfully',
|
||||
});
|
||||
} catch (error) {
|
||||
return res.status(500).json({
|
||||
statusCode: 500,
|
||||
status: false,
|
||||
message: error.message || 'Internal server error',
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// @desc Forgot password
|
||||
exports.forgotPassword = async (req, res, next) => {
|
||||
try {
|
||||
const { email } = req.body;
|
||||
|
||||
if (!email) {
|
||||
return res.status(400).json({
|
||||
statusCode: 400,
|
||||
status: false,
|
||||
message: 'Email is required',
|
||||
});
|
||||
}
|
||||
|
||||
const result = await authService.requestPasswordReset(email);
|
||||
|
||||
return res.status(200).json({
|
||||
statusCode: 200,
|
||||
status: true,
|
||||
message: result.message || 'Password reset email sent successfully',
|
||||
});
|
||||
} catch (error) {
|
||||
return res.status(500).json({
|
||||
statusCode: 500,
|
||||
status: false,
|
||||
message: error.message || 'Internal server error',
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// @desc Reset password
|
||||
exports.resetPassword = async (req, res, next) => {
|
||||
try {
|
||||
const { token, newPassword } = req.body;
|
||||
|
||||
if (!token || !newPassword) {
|
||||
return res.status(400).json({
|
||||
statusCode: 400,
|
||||
status: false,
|
||||
message: 'Token and new password are required',
|
||||
});
|
||||
}
|
||||
|
||||
if (newPassword.length < 6) {
|
||||
return res.status(400).json({
|
||||
statusCode: 400,
|
||||
status: false,
|
||||
message: 'Password must be at least 6 characters',
|
||||
});
|
||||
}
|
||||
|
||||
await authService.resetPassword(token, newPassword);
|
||||
|
||||
return res.status(200).json({
|
||||
statusCode: 200,
|
||||
status: true,
|
||||
message: 'Password reset successfully',
|
||||
});
|
||||
} catch (error) {
|
||||
return res.status(500).json({
|
||||
statusCode: 500,
|
||||
status: false,
|
||||
message: error.message || 'Internal server error',
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// @desc Send verification email
|
||||
exports.sendVerification = async (req, res, next) => {
|
||||
try {
|
||||
await authService.sendVerificationEmail(req.user.id);
|
||||
|
||||
return res.status(200).json({
|
||||
statusCode: 200,
|
||||
status: true,
|
||||
message: 'Verification email sent',
|
||||
});
|
||||
} catch (error) {
|
||||
return res.status(500).json({
|
||||
statusCode: 500,
|
||||
status: false,
|
||||
message: error.message || 'Internal server error',
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// @desc Verify email
|
||||
exports.verifyEmail = async (req, res, next) => {
|
||||
try {
|
||||
const { token } = req.body;
|
||||
|
||||
if (!token) {
|
||||
return res.status(400).json({
|
||||
statusCode: 400,
|
||||
status: false,
|
||||
message: 'Verification token is required',
|
||||
});
|
||||
}
|
||||
|
||||
await authService.verifyEmail(token);
|
||||
|
||||
return res.status(200).json({
|
||||
statusCode: 200,
|
||||
status: true,
|
||||
message: 'Email verified successfully',
|
||||
});
|
||||
} catch (error) {
|
||||
return res.status(500).json({
|
||||
statusCode: 500,
|
||||
status: false,
|
||||
message: error.message || 'Internal server error',
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// @desc Get current user profile
|
||||
exports.getMe = async (req, res, next) => {
|
||||
try {
|
||||
return res.status(200).json({
|
||||
statusCode: 200,
|
||||
status: true,
|
||||
message: 'User retrieved successfully',
|
||||
data: { user: req.user },
|
||||
});
|
||||
} catch (error) {
|
||||
return res.status(500).json({
|
||||
statusCode: 500,
|
||||
status: false,
|
||||
message: error.message || 'Internal server error',
|
||||
});
|
||||
}
|
||||
};
|
||||
351
src/controllers/couponController.js
Normal file
351
src/controllers/couponController.js
Normal file
@@ -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;
|
||||
}
|
||||
|
||||
822
src/controllers/orderController.js
Normal file
822
src/controllers/orderController.js
Normal file
@@ -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 };
|
||||
719
src/controllers/orderTrackingController.js
Normal file
719
src/controllers/orderTrackingController.js
Normal file
@@ -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;
|
||||
}
|
||||
430
src/controllers/payment/paytmController.js
Normal file
430
src/controllers/payment/paytmController.js
Normal file
@@ -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);
|
||||
}
|
||||
};
|
||||
|
||||
811
src/controllers/products/productController.js
Normal file
811
src/controllers/products/productController.js
Normal file
@@ -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' });
|
||||
}
|
||||
};
|
||||
345
src/controllers/products/recommendation.js
Normal file
345
src/controllers/products/recommendation.js
Normal file
@@ -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,
|
||||
};
|
||||
128
src/controllers/users/addressController.js
Normal file
128
src/controllers/users/addressController.js
Normal file
@@ -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);
|
||||
}
|
||||
};
|
||||
70
src/controllers/users/adminUserController.js
Normal file
70
src/controllers/users/adminUserController.js
Normal file
@@ -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);
|
||||
}
|
||||
};
|
||||
150
src/controllers/users/cartController.js
Normal file
150
src/controllers/users/cartController.js
Normal file
@@ -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);
|
||||
}
|
||||
};
|
||||
51
src/controllers/users/orderController.js
Normal file
51
src/controllers/users/orderController.js
Normal file
@@ -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);
|
||||
}
|
||||
};
|
||||
113
src/controllers/users/profileController.js
Normal file
113
src/controllers/users/profileController.js
Normal file
@@ -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);
|
||||
}
|
||||
};
|
||||
|
||||
245
src/controllers/users/wishlistController.js
Normal file
245
src/controllers/users/wishlistController.js
Normal file
@@ -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);
|
||||
}
|
||||
};
|
||||
200
src/controllers/wardrobe/wardrobeItemController.js
Normal file
200
src/controllers/wardrobe/wardrobeItemController.js
Normal file
@@ -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",
|
||||
});
|
||||
}
|
||||
};
|
||||
66
src/controllers/wardrobe/wardrobeMainController.js
Normal file
66
src/controllers/wardrobe/wardrobeMainController.js
Normal file
@@ -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);
|
||||
}
|
||||
};
|
||||
48
src/controllers/wardrobe/wardrobePublicController.js
Normal file
48
src/controllers/wardrobe/wardrobePublicController.js
Normal file
@@ -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);
|
||||
}
|
||||
};
|
||||
29
src/controllers/wardrobe/wardrobeRecommendationController.js
Normal file
29
src/controllers/wardrobe/wardrobeRecommendationController.js
Normal file
@@ -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);
|
||||
}
|
||||
};
|
||||
52
src/controllers/wardrobe/wardrobeSearchController.js
Normal file
52
src/controllers/wardrobe/wardrobeSearchController.js
Normal file
@@ -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);
|
||||
}
|
||||
};
|
||||
39
src/controllers/wardrobe/wardrobeStatsController.js
Normal file
39
src/controllers/wardrobe/wardrobeStatsController.js
Normal file
@@ -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);
|
||||
}
|
||||
};
|
||||
153
src/middleware/auth.js
Normal file
153
src/middleware/auth.js
Normal file
@@ -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,
|
||||
};
|
||||
61
src/middleware/errorHandler.js
Normal file
61
src/middleware/errorHandler.js
Normal file
@@ -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 };
|
||||
10
src/middleware/upload.js
Normal file
10
src/middleware/upload.js
Normal file
@@ -0,0 +1,10 @@
|
||||
const multer = require("multer");
|
||||
|
||||
const upload = multer({
|
||||
storage: multer.memoryStorage(),
|
||||
limits: { fileSize: 5 * 1024 * 1024 },
|
||||
});
|
||||
|
||||
module.exports = upload;
|
||||
|
||||
|
||||
25
src/middleware/uploadProfile.js
Normal file
25
src/middleware/uploadProfile.js
Normal file
@@ -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 };
|
||||
310
src/models/mongodb/Product.js
Normal file
310
src/models/mongodb/Product.js
Normal file
@@ -0,0 +1,310 @@
|
||||
const mongoose = require('mongoose');
|
||||
|
||||
const variantSchema = new mongoose.Schema({
|
||||
size: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
color: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
sku: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
price: {
|
||||
type: Number,
|
||||
required: true,
|
||||
min: 0,
|
||||
},
|
||||
compareAtPrice: {
|
||||
type: Number,
|
||||
min: 0,
|
||||
},
|
||||
inventory: {
|
||||
quantity: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
min: 0,
|
||||
},
|
||||
|
||||
trackInventory: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
},
|
||||
images: [String],
|
||||
isActive: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
});
|
||||
|
||||
const productSchema = new mongoose.Schema({
|
||||
// Basic Information
|
||||
name: {
|
||||
type: String,
|
||||
required: true,
|
||||
trim: true,
|
||||
},
|
||||
slug: {
|
||||
type: String,
|
||||
required: true,
|
||||
unique: true,
|
||||
lowercase: true,
|
||||
},
|
||||
description: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
shortDescription: {
|
||||
type: String,
|
||||
maxLength: 500,
|
||||
},
|
||||
|
||||
// Categorization
|
||||
category: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
subcategory: String,
|
||||
tags: [String],
|
||||
brand: String,
|
||||
|
||||
// Pricing & Inventory
|
||||
basePrice: {
|
||||
type: Number,
|
||||
required: true,
|
||||
min: 0,
|
||||
},
|
||||
compareAtPrice: {
|
||||
type: Number,
|
||||
min: 0,
|
||||
},
|
||||
costPrice: {
|
||||
type: Number,
|
||||
min: 0,
|
||||
},
|
||||
|
||||
// Variants
|
||||
variants: [variantSchema],
|
||||
hasVariants: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
|
||||
// Media
|
||||
images: {
|
||||
primary: String,
|
||||
gallery: [String],
|
||||
videos: [String],
|
||||
},
|
||||
|
||||
// SEO
|
||||
metaTitle: String,
|
||||
metaDescription: String,
|
||||
metaKeywords: [String],
|
||||
|
||||
// Status & Visibility
|
||||
status: {
|
||||
type: String,
|
||||
enum: ['draft', 'active', 'inactive', 'archived'],
|
||||
default: 'draft',
|
||||
},
|
||||
isFeatured: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
isDigital: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
|
||||
// Physical Attributes
|
||||
weight: {
|
||||
value: Number,
|
||||
unit: {
|
||||
type: String,
|
||||
enum: ['g', 'kg', 'lb', 'oz'],
|
||||
default: 'g',
|
||||
},
|
||||
},
|
||||
dimensions: {
|
||||
length: Number,
|
||||
width: Number,
|
||||
height: Number,
|
||||
unit: {
|
||||
type: String,
|
||||
enum: ['cm', 'in'],
|
||||
default: 'cm',
|
||||
},
|
||||
},
|
||||
|
||||
// Analytics
|
||||
viewCount: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
purchaseCount: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
|
||||
// AI Generated Tags
|
||||
aiTags: [String],
|
||||
aiGeneratedDescription: String,
|
||||
|
||||
// Timestamps
|
||||
createdAt: {
|
||||
type: Date,
|
||||
default: Date.now,
|
||||
},
|
||||
updatedAt: {
|
||||
type: Date,
|
||||
default: Date.now,
|
||||
},
|
||||
publishedAt: Date,
|
||||
});
|
||||
|
||||
// Indexes for better performance
|
||||
productSchema.index({ slug: 1 });
|
||||
productSchema.index({ category: 1, status: 1 });
|
||||
productSchema.index({ brand: 1 });
|
||||
productSchema.index({ tags: 1 });
|
||||
productSchema.index({ 'variants.sku': 1 });
|
||||
productSchema.index({ status: 1, isFeatured: 1 });
|
||||
productSchema.index({ createdAt: -1 });
|
||||
productSchema.index({ stock: 1 });
|
||||
|
||||
// Text search index
|
||||
productSchema.index({
|
||||
name: 'text',
|
||||
description: 'text',
|
||||
tags: 'text',
|
||||
brand: 'text',
|
||||
});
|
||||
|
||||
// Virtual for average rating (if implementing ratings)
|
||||
productSchema.virtual('averageRating').get(function () {
|
||||
// This would be calculated from reviews in PostgreSQL
|
||||
return 0;
|
||||
});
|
||||
|
||||
// Pre-save middleware
|
||||
productSchema.pre('save', function (next) {
|
||||
this.updatedAt = new Date();
|
||||
|
||||
// Auto-generate slug if not provided
|
||||
if (!this.slug && this.name) {
|
||||
this.slug = this.name
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9]+/g, '-')
|
||||
.replace(/(^-|-$)/g, '');
|
||||
}
|
||||
|
||||
// Set published date when status changes to active
|
||||
if (
|
||||
this.isModified('status') &&
|
||||
this.status === 'active' &&
|
||||
!this.publishedAt
|
||||
) {
|
||||
this.publishedAt = new Date();
|
||||
}
|
||||
|
||||
next();
|
||||
});
|
||||
|
||||
// Instance methods
|
||||
productSchema.methods.incrementViewCount = function () {
|
||||
this.viewCount += 1;
|
||||
return this.save();
|
||||
};
|
||||
|
||||
productSchema.methods.incrementPurchaseCount = function (quantity = 1) {
|
||||
this.purchaseCount += quantity;
|
||||
return this.save();
|
||||
};
|
||||
|
||||
productSchema.methods.getAvailableVariants = function () {
|
||||
return this.variants.filter(
|
||||
variant =>
|
||||
variant.isActive &&
|
||||
(!variant.inventory.trackInventory || variant.inventory.quantity > 0)
|
||||
);
|
||||
};
|
||||
|
||||
productSchema.methods.getAvailableStock = function () {
|
||||
if (this.hasVariants && this.variants?.length > 0) {
|
||||
return this.variants
|
||||
.filter(v => v.isActive)
|
||||
.reduce((sum, v) => sum + (v.inventory?.quantity || 0), 0);
|
||||
}
|
||||
|
||||
return this.stock || 0;
|
||||
};
|
||||
|
||||
productSchema.methods.isInStock = function (quantity = 1) {
|
||||
const availableStock = this.getAvailableStock();
|
||||
return availableStock >= quantity;
|
||||
};
|
||||
|
||||
productSchema.methods.reduceStock = async function (quantity = 1) {
|
||||
if (this.hasVariants && this.variants && this.variants.length > 0) {
|
||||
return {
|
||||
success: false,
|
||||
message: 'Use variant-specific stock reduction',
|
||||
};
|
||||
}
|
||||
|
||||
const currentStock = this.stock || 0;
|
||||
this.stock = Math.max(0, currentStock - quantity);
|
||||
await this.save();
|
||||
|
||||
return {
|
||||
success: true,
|
||||
previousStock: currentStock,
|
||||
newStock: this.stock,
|
||||
reduced: quantity,
|
||||
};
|
||||
};
|
||||
|
||||
// Static methods
|
||||
productSchema.statics.findBySlug = function (slug) {
|
||||
return this.findOne({ slug, status: 'active' });
|
||||
};
|
||||
|
||||
productSchema.statics.findByCategory = function (
|
||||
category,
|
||||
limit = 20,
|
||||
skip = 0
|
||||
) {
|
||||
return this.find({ category, status: 'active' })
|
||||
.limit(limit)
|
||||
.skip(skip)
|
||||
.sort({ createdAt: -1 });
|
||||
};
|
||||
|
||||
productSchema.statics.searchProducts = function (query, options = {}) {
|
||||
const { category, brand, minPrice, maxPrice, limit = 20, skip = 0 } = options;
|
||||
|
||||
const searchQuery = {
|
||||
$text: { $search: query },
|
||||
status: 'active',
|
||||
};
|
||||
|
||||
if (category) searchQuery.category = category;
|
||||
if (brand) searchQuery.brand = brand;
|
||||
if (minPrice || maxPrice) {
|
||||
searchQuery.basePrice = {};
|
||||
if (minPrice) searchQuery.basePrice.$gte = minPrice;
|
||||
if (maxPrice) searchQuery.basePrice.$lte = maxPrice;
|
||||
}
|
||||
|
||||
return this.find(searchQuery)
|
||||
.limit(limit)
|
||||
.skip(skip)
|
||||
.sort({ score: { $meta: 'textScore' } });
|
||||
};
|
||||
|
||||
module.exports = mongoose.model('Product', productSchema);
|
||||
254
src/models/mongodb/Wardrobe.js
Normal file
254
src/models/mongodb/Wardrobe.js
Normal file
@@ -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);
|
||||
328
src/routes/admin.js
Normal file
328
src/routes/admin.js
Normal file
@@ -0,0 +1,328 @@
|
||||
// const express = require('express');
|
||||
// const multer = require('multer');
|
||||
// const { prisma } = require('../config/database');
|
||||
// const Product = require('../models/mongodb/Product');
|
||||
// const { protect, authorize } = require('../middleware/auth');
|
||||
|
||||
// const dashboard = require('../controllers/admin/dashboardController');
|
||||
// const users = require('../controllers/admin/userController');
|
||||
// const orders = require('../controllers/admin/orderController');
|
||||
// const products = require('../controllers/admin/productController');
|
||||
// const categories = require('../controllers/admin/categoryController');
|
||||
// const coupons = require('../controllers/admin/couponController');
|
||||
|
||||
// const router = express.Router();
|
||||
|
||||
// // ✅ FIXED: Use multer().any() to accept dynamic field names
|
||||
// const upload = multer({
|
||||
// storage: multer.memoryStorage(),
|
||||
// limits: {
|
||||
// fileSize: 5 * 1024 * 1024, // 5MB limit per file
|
||||
// },
|
||||
// });
|
||||
|
||||
// // All routes require admin authentication
|
||||
// router.use(protect);
|
||||
// router.use(authorize('ADMIN'));
|
||||
|
||||
// // @desc Get dashboard statistics
|
||||
// // @route GET /api/admin/dashboard
|
||||
// // @access Private/Admin
|
||||
// router.get('/dashboard', dashboard.getDashboardStats);
|
||||
|
||||
// /**
|
||||
// * @desc Get coupon statistics
|
||||
// * @route GET /api/coupons/admin/stats
|
||||
// * @access Private/Admin
|
||||
// */
|
||||
// router.get(
|
||||
// '/stats',
|
||||
// protect,
|
||||
// authorize('ADMIN', 'SUPER_ADMIN'),
|
||||
// coupons.getCouponStats
|
||||
// );
|
||||
|
||||
// // @desc Get all users with pagination
|
||||
// // @route GET /api/admin/users
|
||||
// // @access Private/Admin
|
||||
// router.get('/users', users.getAllUsers);
|
||||
|
||||
// router.get('/users/:id', users.getUserById);
|
||||
// // @desc Update user status
|
||||
// // @route PUT /api/admin/users/:id/status
|
||||
// // @access Private/Admin
|
||||
// router.put('/users/:id/status', users.updateUserStatus);
|
||||
|
||||
// // @desc Get all orders with filters
|
||||
// // @route GET /api/admin/orders
|
||||
// // @access Private/Admin
|
||||
// router.get('/orders', orders.getAllOrders);
|
||||
|
||||
// /**
|
||||
// * @desc Get status change statistics
|
||||
// * @route GET /api/admin/orders/stats/status-changes
|
||||
// * @access Private/Admin
|
||||
// */
|
||||
// router.get(
|
||||
// '/stats/status-changes',
|
||||
// protect,
|
||||
// authorize('ADMIN', 'SUPER_ADMIN'),
|
||||
// orders.getStatusChangeStats
|
||||
// );
|
||||
|
||||
// /**
|
||||
// * @desc Get single order with full history
|
||||
// * @route GET /api/admin/orders/:orderId
|
||||
// * @access Private/Admin
|
||||
// */
|
||||
// router.get(
|
||||
// '/:orderId',
|
||||
// protect,
|
||||
// authorize('ADMIN', 'SUPER_ADMIN'),
|
||||
// orders.getOrderById
|
||||
// );
|
||||
|
||||
// /**
|
||||
// * @desc Get order status history
|
||||
// * @route GET /api/admin/orders/:orderId/history
|
||||
// * @access Private/Admin
|
||||
// */
|
||||
// router.get(
|
||||
// '/:orderId/history',
|
||||
// protect,
|
||||
// authorize('ADMIN', 'SUPER_ADMIN'),
|
||||
// orders.getOrderStatusHistory
|
||||
// );
|
||||
|
||||
// //Order Details Page
|
||||
// router.get('/orders/:id', orders.getOrderDetails);
|
||||
|
||||
// // @desc Get all products
|
||||
// // @route GET /api/admin/products
|
||||
// // @access Private/Admin
|
||||
// router.get('/products', products.getAllProducts);
|
||||
|
||||
// // @desc Create new product
|
||||
// // @route POST /api/admin/products
|
||||
// // @access Private/Admin
|
||||
// // Create Product Route
|
||||
// router.post(
|
||||
// '/products',
|
||||
// protect,
|
||||
// authorize('ADMIN', 'SUPER_ADMIN'),
|
||||
// upload.any(), // ✅ This accepts ANY field names (including dynamic variant fields)
|
||||
// products.createProduct
|
||||
// );
|
||||
|
||||
// /**
|
||||
// * @desc Get all coupons
|
||||
// * @route GET /api/coupons/admin
|
||||
// * @access Private/Admin
|
||||
// */
|
||||
// router.get(
|
||||
// '/coupons',
|
||||
// protect,
|
||||
// authorize('ADMIN', 'SUPER_ADMIN'),
|
||||
// coupons.getAllCoupons
|
||||
// );
|
||||
|
||||
// // @desc Update product
|
||||
// // @route PUT /api/admin/products/:id
|
||||
// // @access Private/Admin
|
||||
// router.put('/products/:id', products.updateProduct);
|
||||
|
||||
// // @desc Delete product
|
||||
// // @route DELETE /api/admin/products/:id
|
||||
// // @access Private/Admin
|
||||
// router.delete('/products/:id', products.deleteProduct);
|
||||
|
||||
// // @desc Get all categories
|
||||
// // @route GET /api/admin/categories
|
||||
// // @access Private/Admin
|
||||
// router.get('/categories', categories.getAllCategories);
|
||||
|
||||
// // @desc Create new category
|
||||
// // @route POST /api/admin/categories
|
||||
// // @access Private/Admin
|
||||
// router.post('/categories', categories.createCategory);
|
||||
|
||||
// router.put('/categories/:id', categories.updateCategory);
|
||||
|
||||
// router.delete('/categories/:id', categories.deleteCategory);
|
||||
|
||||
// router.patch('/categories/:id/status', categories.toggleCategoryStatus);
|
||||
|
||||
// router.patch('/categories/reorder', categories.reorderCategories);
|
||||
|
||||
// router.get('/categories/:id', categories.getCategoryById);
|
||||
|
||||
// // Category tree / hierarchy
|
||||
// router.get('/tree', categories.getCategoryHierarchy);
|
||||
|
||||
// // @desc Get all coupons
|
||||
// // @route GET /api/admin/coupons
|
||||
// // @access Private/Admin
|
||||
// // router.get('/coupons', coupons.getAllCoupons);
|
||||
|
||||
// // @desc Create new coupon
|
||||
// // @route POST /api/admin/coupons
|
||||
// // @access Private/Admin
|
||||
// // router.post('/coupons', coupons.createCoupon);
|
||||
|
||||
// // ==========================================
|
||||
// // ADMIN ROUTES
|
||||
// // ==========================================
|
||||
|
||||
// /**
|
||||
// * @desc Get single coupon
|
||||
// * @route GET /api/coupons/admin/:id
|
||||
// * @access Private/Admin
|
||||
// */
|
||||
// router.get(
|
||||
// '/coupons/:id',
|
||||
// protect,
|
||||
// authorize('ADMIN', 'SUPER_ADMIN'),
|
||||
// coupons.getCouponById
|
||||
// );
|
||||
|
||||
// /**
|
||||
// * @desc Create coupon
|
||||
// * @route POST /api/coupons/admin
|
||||
// * @access Private/Admin
|
||||
// */
|
||||
// router.post(
|
||||
// '/coupons',
|
||||
// protect,
|
||||
// authorize('ADMIN', 'SUPER_ADMIN'),
|
||||
// coupons.createCoupon
|
||||
// );
|
||||
|
||||
// /**
|
||||
// * @desc Update coupon
|
||||
// * @route PUT /api/coupons/admin/:id
|
||||
// * @access Private/Admin
|
||||
// */
|
||||
// router.put(
|
||||
// '/coupons/:id',
|
||||
// protect,
|
||||
// authorize('ADMIN', 'SUPER_ADMIN'),
|
||||
// coupons.updateCoupon
|
||||
// );
|
||||
|
||||
// /**
|
||||
// * @desc Delete coupon
|
||||
// * @route DELETE /api/coupons/admin/:id
|
||||
// * @access Private/Admin
|
||||
// */
|
||||
// router.delete(
|
||||
// '/coupons/:id',
|
||||
// protect,
|
||||
// authorize('ADMIN', 'SUPER_ADMIN'),
|
||||
// coupons.deleteCoupon
|
||||
// );
|
||||
|
||||
// /**
|
||||
// * @desc Toggle coupon status
|
||||
// * @route PATCH /api/coupons/admin/:id/toggle
|
||||
// * @access Private/Admin
|
||||
// */
|
||||
// router.patch(
|
||||
// '/coupons/:id/toggle',
|
||||
// protect,
|
||||
// authorize('ADMIN', 'SUPER_ADMIN'),
|
||||
// coupons.toggleCouponStatus
|
||||
// );
|
||||
|
||||
// module.exports = router;
|
||||
|
||||
// routes/adminRoutes.js - FIXED VERSION with correct route ordering
|
||||
|
||||
const express = require('express');
|
||||
const multer = require('multer');
|
||||
const { prisma } = require('../config/database');
|
||||
const Product = require('../models/mongodb/Product');
|
||||
const { protect, authorize } = require('../middleware/auth');
|
||||
|
||||
const dashboard = require('../controllers/admin/dashboardController');
|
||||
const users = require('../controllers/admin/userController');
|
||||
const orders = require('../controllers/admin/orderController');
|
||||
const products = require('../controllers/admin/productController');
|
||||
const categories = require('../controllers/admin/categoryController');
|
||||
const coupons = require('../controllers/admin/couponController');
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
const upload = multer({
|
||||
storage: multer.memoryStorage(),
|
||||
limits: {
|
||||
fileSize: 5 * 1024 * 1024,
|
||||
},
|
||||
});
|
||||
|
||||
// All routes require admin authentication
|
||||
router.use(protect);
|
||||
router.use(authorize('ADMIN'));
|
||||
|
||||
// ==========================================
|
||||
// DASHBOARD
|
||||
// ==========================================
|
||||
router.get('/dashboard', dashboard.getDashboardStats);
|
||||
|
||||
// ==========================================
|
||||
// USERS
|
||||
// ==========================================
|
||||
router.get('/users', users.getAllUsers);
|
||||
router.get('/users/:id', users.getUserById);
|
||||
router.put('/users/:id/status', users.updateUserStatus);
|
||||
|
||||
// ==========================================
|
||||
// ORDERS - SPECIFIC ROUTES FIRST
|
||||
// ==========================================
|
||||
router.get('/orders/stats/status-changes', orders.getStatusChangeStats);
|
||||
router.get('/orders', orders.getAllOrders);
|
||||
router.get('/orders/:id', orders.getOrderDetails);
|
||||
router.get('/orders/:orderId/history', orders.getOrderStatusHistory);
|
||||
// router.put('/orders/:orderId/status', orders.updateOrderStatus);
|
||||
|
||||
// ==========================================
|
||||
// PRODUCTS
|
||||
// ==========================================
|
||||
router.get('/products', products.getAllProducts);
|
||||
router.post('/products', upload.any(), products.createProduct);
|
||||
router.put('/products/:id', products.updateProduct);
|
||||
router.delete('/products/:id', products.deleteProduct);
|
||||
|
||||
// ==========================================
|
||||
// CATEGORIES - SPECIFIC ROUTES FIRST
|
||||
// ==========================================
|
||||
router.get('/tree', categories.getCategoryHierarchy);
|
||||
router.patch('/categories/reorder', categories.reorderCategories);
|
||||
router.get('/categories', categories.getAllCategories);
|
||||
// router.post('/categories', categories.createCategory);
|
||||
router.post(
|
||||
'/categories',
|
||||
upload.single('image'), // 👈 THIS IS REQUIRED
|
||||
categories.createCategory
|
||||
);
|
||||
router.get('/categories/:id', categories.getCategoryById);
|
||||
// router.put('/categories/:id', categories.updateCategory);
|
||||
router.put(
|
||||
'/categories/:id',
|
||||
upload.single('image'), // 👈 important
|
||||
categories.updateCategory
|
||||
);
|
||||
router.delete('/categories/:id', categories.deleteCategory);
|
||||
router.patch('/categories/:id/status', categories.toggleCategoryStatus);
|
||||
|
||||
// ==========================================
|
||||
// COUPONS - SPECIFIC ROUTES FIRST
|
||||
// ==========================================
|
||||
router.get('/stats', coupons.getCouponStats);
|
||||
router.get('/coupons', coupons.getAllCoupons);
|
||||
router.post('/coupons', coupons.createCoupon);
|
||||
router.get('/coupons/:id', coupons.getCouponById);
|
||||
router.put('/coupons/:id', coupons.updateCoupon);
|
||||
router.delete('/coupons/:id', coupons.deleteCoupon);
|
||||
router.patch('/coupons/:id/toggle', coupons.toggleCouponStatus);
|
||||
|
||||
module.exports = router;
|
||||
68
src/routes/auth.js
Normal file
68
src/routes/auth.js
Normal file
@@ -0,0 +1,68 @@
|
||||
const express = require('express');
|
||||
const authService = require('../services/authService');
|
||||
const { protect } = require('../middleware/auth');
|
||||
const authController = require('../controllers/authController');
|
||||
const router = express.Router();
|
||||
|
||||
// @desc Register user
|
||||
// @route POST /api/auth/register
|
||||
// @access Public
|
||||
router.post('/register', authController.register);
|
||||
|
||||
// @desc Login user
|
||||
// @route POST /api/auth/login
|
||||
// @access Public
|
||||
router.post('/login', authController.login);
|
||||
|
||||
|
||||
// @desc Send OTP to WhatsApp
|
||||
// @route POST /api/auth/send-otp
|
||||
// @access Public
|
||||
router.post('/send-otp', authController.sendLoginOTP);
|
||||
|
||||
// @desc Verify OTP and login
|
||||
// @route POST /api/auth/verify-otp
|
||||
// @access Public
|
||||
router.post('/verify-otp', authController.verifyLoginOTP);
|
||||
|
||||
// @desc Refresh token
|
||||
// @route POST /api/auth/refresh
|
||||
// @access Public
|
||||
router.post('/refresh', authController.refreshToken);
|
||||
|
||||
// @desc Logout user
|
||||
// @route POST /api/auth/logout
|
||||
// @access Private
|
||||
router.post('/logout', protect, authController.logout);
|
||||
|
||||
// @desc Change password
|
||||
// @route PUT /api/auth/change-password
|
||||
// @access Private
|
||||
router.put('/change-password', protect, authController.changePassword);
|
||||
|
||||
// @desc Request password reset
|
||||
// @route POST /api/auth/forgot-password
|
||||
// @access Public
|
||||
router.post('/forgot-password', authController.forgotPassword);
|
||||
|
||||
// @desc Reset password with token
|
||||
// @route POST /api/auth/reset-password
|
||||
// @access Public
|
||||
router.post('/reset-password', authController.resetPassword);
|
||||
|
||||
// @desc Send verification email
|
||||
// @route POST /api/auth/send-verification
|
||||
// @access Private
|
||||
router.post('/send-verification', protect, authController.sendVerification);
|
||||
|
||||
// @desc Verify email with token
|
||||
// @route POST /api/auth/verify-email
|
||||
// @access Public
|
||||
router.post('/verify-email', authController.verifyEmail);
|
||||
|
||||
// @desc Get current user profile
|
||||
// @route GET /api/auth/me
|
||||
// @access Private
|
||||
router.get('/me', protect, authController.getMe);
|
||||
|
||||
module.exports = router;
|
||||
40
src/routes/couponRoutes.js
Normal file
40
src/routes/couponRoutes.js
Normal file
@@ -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;
|
||||
35
src/routes/deliveryRoutes.js
Normal file
35
src/routes/deliveryRoutes.js
Normal file
@@ -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;
|
||||
100
src/routes/orders.js
Normal file
100
src/routes/orders.js
Normal file
@@ -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;
|
||||
52
src/routes/paymentRoutes.js
Normal file
52
src/routes/paymentRoutes.js
Normal file
@@ -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;
|
||||
137
src/routes/products.js
Normal file
137
src/routes/products.js
Normal file
@@ -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;
|
||||
85
src/routes/reports.js
Normal file
85
src/routes/reports.js
Normal file
@@ -0,0 +1,85 @@
|
||||
// const express = require("express");
|
||||
// const { protect, authorize } = require("../middleware/auth");
|
||||
|
||||
// const reports = require("../controllers/admin/reportController");
|
||||
|
||||
// const router = express.Router();
|
||||
|
||||
// router.use(protect);
|
||||
// router.use(authorize("ADMIN"));
|
||||
|
||||
// // Reports Endpoints
|
||||
// router.get("/overview", reports.getOverviewReport);
|
||||
// router.get("/sales", reports.getSalesAnalytics);
|
||||
// router.get("/customers", reports.getCustomerStats);
|
||||
// router.get("/sellers", reports.getSellerStats);
|
||||
// router.get("/orders", reports.getOrderAnalytics);
|
||||
|
||||
// router.get("/inventory", reports.getInventoryStats);
|
||||
// router.get("/financial", reports.getFinancialStats);
|
||||
|
||||
|
||||
// module.exports = router;
|
||||
|
||||
|
||||
|
||||
const express = require('express');
|
||||
const { protect, authorize } = require('../middleware/auth');
|
||||
const reports = require('../controllers/admin/reportController');
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
// ─── Auth Guard ───────────────────────────────
|
||||
router.use(protect);
|
||||
router.use(authorize('ADMIN'));
|
||||
|
||||
// ─── Overview ────────────────────────────────
|
||||
// GET /api/admin/reports/overview?from=2024-01-01&to=2024-12-31
|
||||
router.get('/overview', reports.getOverviewReport);
|
||||
|
||||
// ─── Sales ───────────────────────────────────
|
||||
// GET /api/admin/reports/sales?from=&to=&groupBy=daily|monthly
|
||||
router.get('/sales', reports.getSalesAnalytics);
|
||||
|
||||
// ─── Customers ───────────────────────────────
|
||||
// GET /api/admin/reports/customers?view=weekly|monthly|yearly
|
||||
router.get('/customers', reports.getCustomerStats);
|
||||
|
||||
// ─── Sellers ─────────────────────────────────
|
||||
// GET /api/admin/reports/sellers?page=1&limit=20
|
||||
router.get('/sellers', reports.getSellerStats);
|
||||
|
||||
// ─── Orders ──────────────────────────────────
|
||||
// GET /api/admin/reports/orders?from=&to=
|
||||
router.get('/orders', reports.getOrderAnalytics);
|
||||
|
||||
// ─── Inventory ───────────────────────────────
|
||||
// GET /api/admin/reports/inventory?page=1&limit=20
|
||||
router.get('/inventory', reports.getInventoryStats);
|
||||
|
||||
// ─── Financial ───────────────────────────────
|
||||
// GET /api/admin/reports/financial?from=&to=
|
||||
router.get('/financial', reports.getFinancialStats);
|
||||
|
||||
// ─── Payouts ─────────────────────────────────
|
||||
// GET /api/admin/reports/payouts?page=1&limit=20&from=&to=
|
||||
router.get('/payouts', reports.getPayoutHistory);
|
||||
|
||||
// ─── Activity Feed ────────────────────────────
|
||||
// GET /api/admin/reports/activity?limit=20
|
||||
// Requires ActivityLog model in Prisma schema (see note in controller)
|
||||
router.get('/activity', reports.getActivityFeed);
|
||||
|
||||
// ─── Top Products ────────────────────────────
|
||||
// GET /api/admin/reports/top-products?limit=10
|
||||
router.get('/top-products', reports.getTopProducts);
|
||||
|
||||
// ─── Coupon Stats ────────────────────────────
|
||||
// GET /api/admin/reports/coupons?from=&to=
|
||||
router.get('/coupons', reports.getCouponStats);
|
||||
|
||||
// ─── Returns (uncomment when ReturnModel is ready) ───
|
||||
// GET /api/admin/reports/returns?from=&to=
|
||||
// router.get('/returns', reports.getReturnAnalytics);
|
||||
|
||||
module.exports = router;
|
||||
17
src/routes/upload.routes.js
Normal file
17
src/routes/upload.routes.js
Normal file
@@ -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;
|
||||
550
src/routes/users.js
Normal file
550
src/routes/users.js
Normal file
@@ -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;
|
||||
393
src/routes/wardrobe.js
Normal file
393
src/routes/wardrobe.js
Normal file
@@ -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;
|
||||
149
src/scripts/addStockFieldToProducts.js
Normal file
149
src/scripts/addStockFieldToProducts.js
Normal file
@@ -0,0 +1,149 @@
|
||||
// scripts/addStockFieldToProducts.js - MIGRATION SCRIPT
|
||||
|
||||
const mongoose = require('mongoose');
|
||||
require('dotenv').config();
|
||||
|
||||
/**
|
||||
* Add stock field to all existing products in MongoDB
|
||||
*/
|
||||
async function addStockFieldToProducts() {
|
||||
try {
|
||||
console.log('🔄 Starting stock field migration...\n');
|
||||
|
||||
// Connect to MongoDB
|
||||
await mongoose.connect(process.env.MONGODB_URI || process.env.MONGO_URI);
|
||||
console.log('✅ Connected to MongoDB\n');
|
||||
|
||||
const db = mongoose.connection.db;
|
||||
const productsCollection = db.collection('products');
|
||||
|
||||
// Get all products
|
||||
const allProducts = await productsCollection.find({}).toArray();
|
||||
console.log(`📦 Found ${allProducts.length} total products\n`);
|
||||
|
||||
// Separate products by variant status
|
||||
const withVariants = allProducts.filter(p => p.hasVariants && p.variants?.length > 0);
|
||||
const withoutVariants = allProducts.filter(p => !p.hasVariants || !p.variants?.length);
|
||||
|
||||
console.log(` ${withVariants.length} products with variants`);
|
||||
console.log(` ${withoutVariants.length} products without variants\n`);
|
||||
|
||||
let updated = 0;
|
||||
let skipped = 0;
|
||||
|
||||
// Update products WITHOUT variants
|
||||
console.log('🔄 Adding stock field to simple products...\n');
|
||||
|
||||
for (const product of withoutVariants) {
|
||||
// Skip if stock field already exists
|
||||
if (product.stock !== undefined && product.stock !== null) {
|
||||
console.log(` ⏭️ ${product.name}: Already has stock (${product.stock})`);
|
||||
skipped++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Add stock field (default 50 for existing products)
|
||||
const DEFAULT_STOCK = 50;
|
||||
|
||||
await productsCollection.updateOne(
|
||||
{ _id: product._id },
|
||||
{
|
||||
$set: {
|
||||
stock: DEFAULT_STOCK,
|
||||
trackInventory: true,
|
||||
updatedAt: new Date()
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
console.log(` ✅ ${product.name}: stock = ${DEFAULT_STOCK}`);
|
||||
updated++;
|
||||
}
|
||||
|
||||
// For products WITH variants, set stock to 0 (they use variant inventory)
|
||||
console.log(`\n🔄 Setting stock=0 for variant products...\n`);
|
||||
|
||||
for (const product of withVariants) {
|
||||
await productsCollection.updateOne(
|
||||
{ _id: product._id },
|
||||
{
|
||||
$set: {
|
||||
stock: 0,
|
||||
trackInventory: false,
|
||||
updatedAt: new Date()
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
console.log(` ✅ ${product.name}: stock = 0 (uses variants)`);
|
||||
updated++;
|
||||
}
|
||||
|
||||
// Verification
|
||||
console.log(`\n📊 Verifying migration...\n`);
|
||||
|
||||
const verification = await productsCollection.aggregate([
|
||||
{
|
||||
$group: {
|
||||
_id: null,
|
||||
totalProducts: { $sum: 1 },
|
||||
withStock: {
|
||||
$sum: {
|
||||
$cond: [{ $ifNull: ['$stock', false] }, 1, 0]
|
||||
}
|
||||
},
|
||||
avgStock: { $avg: '$stock' },
|
||||
totalStock: { $sum: '$stock' }
|
||||
}
|
||||
}
|
||||
]).toArray();
|
||||
|
||||
if (verification.length > 0) {
|
||||
const stats = verification[0];
|
||||
console.log(` Total Products: ${stats.totalProducts}`);
|
||||
console.log(` With Stock Field: ${stats.withStock}`);
|
||||
console.log(` Average Stock: ${Math.round(stats.avgStock || 0)}`);
|
||||
console.log(` Total Stock: ${stats.totalStock || 0}`);
|
||||
}
|
||||
|
||||
console.log(`\n✅ Migration completed!`);
|
||||
console.log(` Updated: ${updated} products`);
|
||||
console.log(` Skipped: ${skipped} products (already had stock)\n`);
|
||||
|
||||
// Stock distribution
|
||||
const stockDistribution = await productsCollection.aggregate([
|
||||
{
|
||||
$bucket: {
|
||||
groupBy: '$stock',
|
||||
boundaries: [0, 1, 6, 11, 51],
|
||||
default: 'Other',
|
||||
output: {
|
||||
count: { $sum: 1 },
|
||||
products: { $push: '$name' }
|
||||
}
|
||||
}
|
||||
}
|
||||
]).toArray();
|
||||
|
||||
console.log('📈 Stock Distribution:');
|
||||
stockDistribution.forEach(bucket => {
|
||||
const range = bucket._id === 'Other' ? 'Other' :
|
||||
bucket._id === 0 ? '0 (Out of Stock)' :
|
||||
bucket._id === 1 ? '1-5 (Critical)' :
|
||||
bucket._id === 6 ? '6-10 (Low)' :
|
||||
'11-50 (In Stock)';
|
||||
console.log(` ${range}: ${bucket.count} products`);
|
||||
});
|
||||
|
||||
await mongoose.connection.close();
|
||||
console.log('\n✅ Database connection closed');
|
||||
process.exit(0);
|
||||
|
||||
} catch (error) {
|
||||
console.error('\n❌ Migration error:', error);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
// Run migration
|
||||
addStockFieldToProducts();
|
||||
48
src/scripts/migrateCategoryIds.js
Normal file
48
src/scripts/migrateCategoryIds.js
Normal file
@@ -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();
|
||||
149
src/server.js
Normal file
149
src/server.js
Normal file
@@ -0,0 +1,149 @@
|
||||
require('dotenv').config();
|
||||
// import uploadRoutes from "./routes/upload.routes";
|
||||
const uploadRoutes = require('./routes/upload.routes');
|
||||
|
||||
const express = require('express');
|
||||
const cors = require('cors');
|
||||
const helmet = require('helmet');
|
||||
const morgan = require('morgan');
|
||||
const cookieParser = require('cookie-parser');
|
||||
|
||||
const {
|
||||
initializeDatabases,
|
||||
closeDatabaseConnections,
|
||||
} = require('./config/database');
|
||||
const { errorHandler, notFound } = require('./middleware/errorHandler');
|
||||
|
||||
const app = express();
|
||||
const PORT = process.env.PORT || 3000;
|
||||
|
||||
// Security middleware
|
||||
app.use(
|
||||
helmet({
|
||||
contentSecurityPolicy: false, // Disable for API
|
||||
crossOriginEmbedderPolicy: false,
|
||||
})
|
||||
);
|
||||
|
||||
const allowedOrigins = process.env.CORS_ORIGIN
|
||||
? process.env.CORS_ORIGIN.split(',').map(origin => origin.trim())
|
||||
: [];
|
||||
|
||||
const corsOptions = {
|
||||
origin: function (origin, callback) {
|
||||
// Allow requests with no origin (like Postman, mobile apps)
|
||||
if (!origin) return callback(null, true);
|
||||
|
||||
if (allowedOrigins.includes(origin)) {
|
||||
callback(null, true);
|
||||
} else {
|
||||
callback(new Error('Not allowed by CORS'));
|
||||
}
|
||||
},
|
||||
credentials: true,
|
||||
methods: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'OPTIONS'],
|
||||
allowedHeaders: ['Content-Type', 'Authorization', 'X-Requested-With'],
|
||||
};
|
||||
|
||||
|
||||
app.use(cors(corsOptions));
|
||||
|
||||
// Body parsing middleware
|
||||
app.use(express.json({ limit: '10mb' }));
|
||||
app.use(express.urlencoded({ extended: true, limit: '10mb' }));
|
||||
app.use(cookieParser());
|
||||
|
||||
// Logging middleware
|
||||
if (process.env.NODE_ENV !== 'test') {
|
||||
app.use(morgan('combined'));
|
||||
}
|
||||
|
||||
// Health check endpoint
|
||||
app.get('/health', async (req, res) => {
|
||||
try {
|
||||
res.status(200).json({
|
||||
status: 'SUCCESS',
|
||||
message: 'Vaishnavi Creation API is running successfully 🚀',
|
||||
service: 'vaishnavi-backend',
|
||||
version: '1.0.0',
|
||||
environment: process.env.NODE_ENV,
|
||||
timestamp: new Date().toISOString(),
|
||||
uptime_seconds: process.uptime(),
|
||||
memory_usage: process.memoryUsage().rss, // RAM usage
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(500).json({
|
||||
status: 'ERROR',
|
||||
message: 'Health check failed',
|
||||
error: error.message,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// API Routes
|
||||
app.use('/api/auth', require('./routes/auth'));
|
||||
app.use('/api/users', require('./routes/users'));
|
||||
app.use('/api/products', require('./routes/products'));
|
||||
app.use('/api/orders', require('./routes/orders'));
|
||||
app.use('/api/wardrobe', require('./routes/wardrobe'));
|
||||
app.use('/api/delivery', require('./routes/deliveryRoutes'));
|
||||
app.use('/api/coupons', require('./routes/couponRoutes'));
|
||||
app.use('/api/admin', require('./routes/admin'));
|
||||
|
||||
app.use('/api/admin/reports', require('./routes/reports'));
|
||||
app.use('/api/payments', require('./routes/paymentRoutes'));
|
||||
// Upload route
|
||||
app.use('/api', uploadRoutes);
|
||||
|
||||
// Root endpoint
|
||||
app.get('/', (req, res) => {
|
||||
res.json({
|
||||
message: 'Vaishnavi Creation API',
|
||||
version: '1.0.0',
|
||||
documentation: '/api/docs',
|
||||
health: '/health',
|
||||
});
|
||||
});
|
||||
|
||||
// Error handling middleware (must be last)
|
||||
app.use(notFound);
|
||||
app.use(errorHandler);
|
||||
|
||||
// Graceful shutdown
|
||||
process.on('SIGTERM', async () => {
|
||||
console.log('SIGTERM received, shutting down gracefully');
|
||||
await closeDatabaseConnections();
|
||||
process.exit(0);
|
||||
});
|
||||
|
||||
process.on('SIGINT', async () => {
|
||||
console.log('SIGINT received, shutting down gracefully');
|
||||
await closeDatabaseConnections();
|
||||
process.exit(0);
|
||||
});
|
||||
|
||||
// Start server
|
||||
const startServer = async () => {
|
||||
try {
|
||||
// Initialize database connections
|
||||
await initializeDatabases();
|
||||
|
||||
// Start the server
|
||||
app.listen(PORT, () => {
|
||||
console.log(`🚀 Server running on port ${PORT}`);
|
||||
console.log(`📚 API Documentation: http://localhost:${PORT}/api/docs`);
|
||||
console.log(`🏥 Health Check: http://localhost:${PORT}/health`);
|
||||
console.log(`🌍 Environment: ${process.env.NODE_ENV}`);
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('❌ Failed to start server:', error);
|
||||
process.exit(1);
|
||||
}
|
||||
};
|
||||
|
||||
// Only start server if this file is run directly
|
||||
if (require.main === module) {
|
||||
startServer();
|
||||
}
|
||||
|
||||
module.exports = app;
|
||||
348
src/services/authService.js
Normal file
348
src/services/authService.js
Normal file
@@ -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();
|
||||
266
src/services/deliveryEstimationService.js
Normal file
266
src/services/deliveryEstimationService.js
Normal file
@@ -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,
|
||||
};
|
||||
264
src/services/inventoryService.js
Normal file
264
src/services/inventoryService.js
Normal file
@@ -0,0 +1,264 @@
|
||||
// services/inventoryService.js - COMPLETE INVENTORY SYSTEM
|
||||
const { prisma } = require('../config/database');
|
||||
const Product = require('../models/mongodb/Product');
|
||||
|
||||
/**
|
||||
* ✅ Auto-reduce stock when order is DELIVERED
|
||||
*/
|
||||
async function reduceStockOnDelivery(orderId) {
|
||||
try {
|
||||
const order = await prisma.order.findUnique({
|
||||
where: { id: orderId },
|
||||
include: { items: true },
|
||||
});
|
||||
|
||||
if (!order || order.status !== 'DELIVERED') return null;
|
||||
|
||||
const results = [];
|
||||
|
||||
for (const item of order.items) {
|
||||
const product = await Product.findById(item.productId);
|
||||
if (!product) continue;
|
||||
|
||||
let previousStock, newStock;
|
||||
|
||||
if (product.hasVariants && product.variants?.length > 0) {
|
||||
// ✅ Reduce from matching variant by SKU
|
||||
previousStock = product.variants
|
||||
.filter(v => v.isActive)
|
||||
.reduce((sum, v) => sum + (v.inventory?.quantity || 0), 0);
|
||||
|
||||
const variantIndex = product.variants.findIndex(
|
||||
v => v.sku === item.productSku
|
||||
);
|
||||
|
||||
if (variantIndex !== -1) {
|
||||
const currentQty =
|
||||
product.variants[variantIndex].inventory?.quantity || 0;
|
||||
product.variants[variantIndex].inventory.quantity = Math.max(
|
||||
0,
|
||||
currentQty - item.quantity
|
||||
);
|
||||
await product.save();
|
||||
}
|
||||
|
||||
newStock = product.variants
|
||||
.filter(v => v.isActive)
|
||||
.reduce((sum, v) => sum + (v.inventory?.quantity || 0), 0);
|
||||
} else {
|
||||
// ✅ Non-variant product
|
||||
previousStock = product.stock || 0;
|
||||
newStock = Math.max(0, previousStock - item.quantity);
|
||||
await Product.findByIdAndUpdate(item.productId, {
|
||||
stock: newStock,
|
||||
updatedAt: new Date(),
|
||||
});
|
||||
}
|
||||
|
||||
await prisma.inventoryLog.create({
|
||||
data: {
|
||||
productId: item.productId,
|
||||
productName: item.productName || product.name,
|
||||
type: 'SOLD',
|
||||
quantityChange: -item.quantity,
|
||||
previousStock,
|
||||
newStock,
|
||||
orderId,
|
||||
notes: `Order ${order.orderNumber} delivered`,
|
||||
},
|
||||
});
|
||||
|
||||
results.push({
|
||||
productId: item.productId,
|
||||
productName: product.name,
|
||||
reduced: item.quantity,
|
||||
previousStock,
|
||||
newStock,
|
||||
});
|
||||
}
|
||||
|
||||
return results;
|
||||
} catch (error) {
|
||||
console.error('❌ Stock reduction error:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* ✅ Get low stock products
|
||||
*/
|
||||
async function getLowStockProducts(threshold = 10) {
|
||||
try {
|
||||
const products = await Product.find({ status: 'active' }).lean();
|
||||
|
||||
const withStock = products.map(product => {
|
||||
let totalStock = 0;
|
||||
|
||||
if (product.hasVariants && product.variants?.length > 0) {
|
||||
totalStock = product.variants
|
||||
.filter(v => v.isActive)
|
||||
.reduce((sum, v) => sum + (v.inventory?.quantity || 0), 0);
|
||||
} else {
|
||||
totalStock = product.stock || 0;
|
||||
}
|
||||
|
||||
return {
|
||||
_id: product._id.toString(),
|
||||
name: product.name,
|
||||
slug: product.slug,
|
||||
stock: totalStock,
|
||||
basePrice: product.basePrice,
|
||||
hasVariants: product.hasVariants,
|
||||
status:
|
||||
totalStock === 0
|
||||
? 'OUT_OF_STOCK'
|
||||
: totalStock <= 5
|
||||
? 'CRITICAL'
|
||||
: 'LOW',
|
||||
displayImage: getProductImage(product),
|
||||
};
|
||||
});
|
||||
|
||||
return withStock
|
||||
.filter(p => p.stock <= threshold)
|
||||
.sort((a, b) => a.stock - b.stock)
|
||||
.slice(0, threshold);
|
||||
} catch (error) {
|
||||
console.error('Error fetching low stock:', error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* ✅ Get inventory stats for dashboard
|
||||
*/
|
||||
async function getInventoryStats() {
|
||||
try {
|
||||
const products = await Product.find({ status: 'active' }).lean();
|
||||
|
||||
let outOfStock = 0,
|
||||
criticalStock = 0,
|
||||
lowStock = 0,
|
||||
inStock = 0;
|
||||
|
||||
products.forEach(product => {
|
||||
let stock = 0;
|
||||
|
||||
if (product.hasVariants && product.variants?.length > 0) {
|
||||
stock = product.variants
|
||||
.filter(v => v.isActive)
|
||||
.reduce((sum, v) => sum + (v.inventory?.quantity || 0), 0);
|
||||
} else {
|
||||
stock = product.stock || 0;
|
||||
}
|
||||
|
||||
if (stock === 0) outOfStock++;
|
||||
else if (stock <= 5) criticalStock++;
|
||||
else if (stock <= 10) lowStock++;
|
||||
else inStock++;
|
||||
});
|
||||
|
||||
return {
|
||||
totalProducts: products.length,
|
||||
outOfStock,
|
||||
criticalStock,
|
||||
lowStock,
|
||||
inStock,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Error fetching inventory stats:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* ✅ Manual stock adjustment (Admin)
|
||||
*/
|
||||
async function adjustStock(
|
||||
productId,
|
||||
variantSku = null,
|
||||
quantity,
|
||||
type,
|
||||
notes,
|
||||
adminId
|
||||
) {
|
||||
try {
|
||||
const product = await Product.findById(productId);
|
||||
if (!product) throw new Error('Product not found');
|
||||
|
||||
let previousStock, newStock;
|
||||
|
||||
if (product.hasVariants && product.variants?.length > 0 && variantSku) {
|
||||
// ✅ Adjust specific variant
|
||||
const variantIndex = product.variants.findIndex(
|
||||
v => v.sku === variantSku
|
||||
);
|
||||
if (variantIndex === -1) throw new Error('Variant not found');
|
||||
|
||||
const currentQty =
|
||||
product.variants[variantIndex].inventory?.quantity || 0;
|
||||
previousStock = currentQty;
|
||||
|
||||
if (type === 'ADD') newStock = currentQty + quantity;
|
||||
else if (type === 'REMOVE') newStock = Math.max(0, currentQty - quantity);
|
||||
else if (type === 'SET') newStock = quantity;
|
||||
else throw new Error('Invalid type');
|
||||
|
||||
product.variants[variantIndex].inventory.quantity = newStock;
|
||||
await product.save();
|
||||
} else {
|
||||
// ✅ Non-variant product
|
||||
previousStock = product.stock || 0;
|
||||
|
||||
if (type === 'ADD') newStock = previousStock + quantity;
|
||||
else if (type === 'REMOVE')
|
||||
newStock = Math.max(0, previousStock - quantity);
|
||||
else if (type === 'SET') newStock = quantity;
|
||||
else throw new Error('Invalid type');
|
||||
|
||||
await Product.findByIdAndUpdate(productId, {
|
||||
stock: newStock,
|
||||
updatedAt: new Date(),
|
||||
});
|
||||
}
|
||||
|
||||
await prisma.inventoryLog.create({
|
||||
data: {
|
||||
productId,
|
||||
productName: product.name,
|
||||
type: type === 'ADD' ? 'RESTOCK' : 'ADJUSTMENT',
|
||||
quantityChange: type === 'ADD' ? quantity : -quantity,
|
||||
previousStock,
|
||||
newStock,
|
||||
notes: notes || `Manual ${type} by admin`,
|
||||
adjustedBy: adminId,
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
success: true,
|
||||
productName: product.name,
|
||||
previousStock,
|
||||
newStock,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Error adjusting stock:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
function getProductImage(product) {
|
||||
return (
|
||||
product.images?.gallery?.[0] ||
|
||||
product.images?.primary ||
|
||||
product.variants?.[0]?.images?.[0] ||
|
||||
'https://via.placeholder.com/300'
|
||||
);
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
reduceStockOnDelivery,
|
||||
getLowStockProducts,
|
||||
getInventoryStats,
|
||||
adjustStock,
|
||||
};
|
||||
59
src/services/otpStore.js
Normal file
59
src/services/otpStore.js
Normal file
@@ -0,0 +1,59 @@
|
||||
// Simple in-memory OTP store
|
||||
// Replace with Redis in production: await redis.setex(key, 300, otp)
|
||||
const otpStore = new Map();
|
||||
|
||||
const OTP_EXPIRY_MS = 5 * 60 * 1000; // 5 minutes
|
||||
|
||||
// ✅ TEST OTP — works in any environment when used
|
||||
const TEST_OTP = '123456';
|
||||
const TEST_PHONES = ['9999999999']; // add any test phone numbers here
|
||||
|
||||
|
||||
|
||||
function saveOTP(phone, otp) {
|
||||
otpStore.set(phone, {
|
||||
otp,
|
||||
expiresAt: Date.now() + OTP_EXPIRY_MS,
|
||||
attempts: 0,
|
||||
});
|
||||
}
|
||||
|
||||
function verifyOTP(phone, inputOtp) {
|
||||
// ✅ Master bypass OTP — always works in development
|
||||
if (process.env.NODE_ENV === 'development' && inputOtp === TEST_OTP) {
|
||||
console.log(`🔓 Test OTP used for ${phone}`);
|
||||
return { valid: true };
|
||||
}
|
||||
|
||||
// ✅ Test phone numbers skip WhatsApp entirely
|
||||
if (TEST_PHONES.includes(phone) && inputOtp === TEST_OTP) {
|
||||
return { valid: true };
|
||||
}
|
||||
|
||||
// Normal OTP verification below...
|
||||
const record = otpStore.get(phone);
|
||||
if (!record) return { valid: false, reason: 'OTP not found or expired' };
|
||||
if (Date.now() > record.expiresAt) {
|
||||
otpStore.delete(phone);
|
||||
return { valid: false, reason: 'OTP expired' };
|
||||
}
|
||||
|
||||
record.attempts++;
|
||||
if (record.attempts > 3) {
|
||||
otpStore.delete(phone);
|
||||
return { valid: false, reason: 'Too many attempts' };
|
||||
}
|
||||
|
||||
if (record.otp !== inputOtp) {
|
||||
return { valid: false, reason: 'Invalid OTP' };
|
||||
}
|
||||
|
||||
otpStore.delete(phone);
|
||||
return { valid: true };
|
||||
}
|
||||
|
||||
function clearOTP(phone) {
|
||||
otpStore.delete(phone);
|
||||
}
|
||||
|
||||
module.exports = { saveOTP, verifyOTP, clearOTP };
|
||||
25
src/services/s3Upload.service.js
Normal file
25
src/services/s3Upload.service.js
Normal file
@@ -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}`;
|
||||
};
|
||||
63
src/services/wappconnectService.js
Normal file
63
src/services/wappconnectService.js
Normal file
@@ -0,0 +1,63 @@
|
||||
const axios = require('axios');
|
||||
|
||||
const TEST_PHONES = ['9999999999'];
|
||||
const TEST_OTP = '123456';
|
||||
|
||||
// Generate 6-digit OTP
|
||||
function generateOTP() {
|
||||
return Math.floor(100000 + Math.random() * 900000).toString();
|
||||
}
|
||||
|
||||
// Send OTP via WhatsApp
|
||||
async function sendOTP(phoneNumber, otp) {
|
||||
// Skip real API for test phones
|
||||
if (TEST_PHONES.includes(phoneNumber)) {
|
||||
console.log(`🧪 Test phone — Use OTP: ${TEST_OTP}`);
|
||||
return { success: true, test: true };
|
||||
}
|
||||
|
||||
try {
|
||||
// Format phone number to 12 digits (91XXXXXXXXXX)
|
||||
const digits = phoneNumber.replace(/\D/g, '');
|
||||
const formatted =
|
||||
digits.startsWith('91') && digits.length === 12 ? digits : `91${digits}`;
|
||||
|
||||
// ✅ Same variable name as karaychakra: WAPP_TOKEN
|
||||
const WAPP_TOKEN = process.env.WAPP_TOKEN;
|
||||
// const message = `🔐 Your OTP is: *${otp}*\n\nValid for 5 minutes. Do not share with anyone.`;
|
||||
const message = `🌸 *Vaishnavi Creation* 🌸
|
||||
|
||||
To continue your login, please use the OTP below:
|
||||
|
||||
🔑 *${otp}*
|
||||
|
||||
⏳ Valid for the next *5 minutes*.
|
||||
|
||||
If you did not request this OTP, please ignore this message.
|
||||
|
||||
Thank you for choosing Vaishnavi Creation. 💫`;
|
||||
|
||||
console.log('📞 Sending to:', formatted);
|
||||
console.log(
|
||||
'🔑 WAPP_TOKEN:',
|
||||
WAPP_TOKEN ? WAPP_TOKEN.substring(0, 10) + '...' : '❌ MISSING'
|
||||
);
|
||||
|
||||
// ✅ Exact same logic as karaychakra
|
||||
const response = await axios({
|
||||
method: 'get',
|
||||
url: `https://api.wappconnect.com/api/sendText?token=${WAPP_TOKEN}&phone=${formatted}&message=${message}`,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
});
|
||||
|
||||
console.log('✅ WhatsApp sent:', response.data);
|
||||
return { success: true, data: response.data };
|
||||
} catch (error) {
|
||||
console.error('❌ Status:', error?.response?.status);
|
||||
console.error('❌ Body:', JSON.stringify(error?.response?.data));
|
||||
console.error('❌ Real error:', error.message);
|
||||
throw new Error('Failed to send WhatsApp OTP');
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { generateOTP, sendOTP, TEST_OTP, TEST_PHONES };
|
||||
34
src/utils/mailer.js
Normal file
34
src/utils/mailer.js
Normal file
@@ -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;
|
||||
315
src/utils/paytm.js
Normal file
315
src/utils/paytm.js
Normal file
@@ -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,
|
||||
};
|
||||
23
src/utils/uploadToS3.js
Normal file
23
src/utils/uploadToS3.js
Normal file
@@ -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;
|
||||
35
src/utils/whatsapp.js
Normal file
35
src/utils/whatsapp.js
Normal file
@@ -0,0 +1,35 @@
|
||||
// utils/whatsapp.js
|
||||
const axios = require('axios');
|
||||
|
||||
const sendOTP = async (phone, otp) => {
|
||||
const baseUrl = process.env.WAPP_BASE_URL;
|
||||
const token = process.env.WAPP_TOKEN;
|
||||
const instance = process.env.WAPP_INSTANCE; // SSM
|
||||
|
||||
const formattedPhone = `${phone}@c.us`;
|
||||
const message = `Your OTP is: *${otp}*\nValid for 5 minutes.`;
|
||||
|
||||
// 👇 These 3 lines will tell us exactly what's wrong
|
||||
console.log('BASE_URL:', baseUrl);
|
||||
console.log('TOKEN:', token ? token.substring(0, 10) + '...' : 'MISSING ❌');
|
||||
console.log('INSTANCE:', instance || 'MISSING ❌');
|
||||
|
||||
const url = `${baseUrl}/api/${token}/${instance}/send-message`;
|
||||
console.log('Calling URL:', url);
|
||||
|
||||
try {
|
||||
const response = await axios.post(url,
|
||||
{ phone: formattedPhone, message },
|
||||
{ headers: { 'Content-Type': 'application/json' } }
|
||||
);
|
||||
console.log('✅ WappConnect response:', JSON.stringify(response.data));
|
||||
return response.data;
|
||||
} catch (err) {
|
||||
console.log('❌ Status:', err.response?.status);
|
||||
console.log('❌ Response body:', JSON.stringify(err.response?.data));
|
||||
console.log('❌ Full URL was:', url);
|
||||
throw new Error('Failed to send WhatsApp OTP');
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = { sendOTP };
|
||||
35
src/views/emails/reset-password.ejs
Normal file
35
src/views/emails/reset-password.ejs
Normal file
@@ -0,0 +1,35 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Password Reset</title>
|
||||
</head>
|
||||
<body style="margin:0; padding:0; font-family: Arial, sans-serif; background-color: #f4f4f4;">
|
||||
<table align="center" width="100%" cellpadding="0" cellspacing="0" style="max-width:600px; margin:20px auto; background-color:#ffffff; border-radius:8px; overflow:hidden; box-shadow:0 4px 8px rgba(0,0,0,0.1);">
|
||||
<tr>
|
||||
<td style="background-color:#4CAF50; padding:20px; text-align:center; color:#ffffff; font-size:24px; font-weight:bold;">
|
||||
VC E-Commerce
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding:30px; text-align:left; color:#333333;">
|
||||
<h2 style="color:#4CAF50;">Password Reset Request</h2>
|
||||
<p>Hi <%= firstName %>,</p>
|
||||
<p>You recently requested to reset your password. Click the button below to reset it. This link is valid for 1 hour only.</p>
|
||||
<p style="text-align:center; margin:30px 0;">
|
||||
<a href="<%= resetUrl %>" style="background-color:#4CAF50; color:#ffffff; text-decoration:none; padding:15px 25px; border-radius:5px; display:inline-block; font-weight:bold;">
|
||||
Reset Password
|
||||
</a>
|
||||
</p>
|
||||
<p>If you did not request a password reset, please ignore this email.</p>
|
||||
<p>Thanks,<br>The VC E-Commerce Team</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="background-color:#f4f4f4; padding:20px; text-align:center; color:#777777; font-size:12px;">
|
||||
© <%= new Date().getFullYear() %> VC E-Commerce. All rights reserved.
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</body>
|
||||
</html>
|
||||
35
src/views/emails/verify-email.ejs
Normal file
35
src/views/emails/verify-email.ejs
Normal file
@@ -0,0 +1,35 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Email Verification</title>
|
||||
</head>
|
||||
<body style="margin:0; padding:0; font-family: Arial, sans-serif; background-color: #f4f4f4;">
|
||||
<table align="center" width="100%" cellpadding="0" cellspacing="0" style="max-width:600px; margin:20px auto; background-color:#ffffff; border-radius:8px; overflow:hidden; box-shadow:0 4px 8px rgba(0,0,0,0.1);">
|
||||
<tr>
|
||||
<td style="background-color:#4CAF50; padding:20px; text-align:center; color:#ffffff; font-size:24px; font-weight:bold;">
|
||||
VC E-Commerce
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding:30px; text-align:left; color:#333333;">
|
||||
<h2 style="color:#4CAF50;">Email Verification</h2>
|
||||
<p>Hi <%= firstName %>,</p>
|
||||
<p>Thank you for registering! Please click the button below to verify your email address and activate your account.</p>
|
||||
<p style="text-align:center; margin:30px 0;">
|
||||
<a href="<%= verificationUrl %>" style="background-color:#4CAF50; color:#ffffff; text-decoration:none; padding:15px 25px; border-radius:5px; display:inline-block; font-weight:bold;">
|
||||
Verify Email
|
||||
</a>
|
||||
</p>
|
||||
<p>If you did not register, you can safely ignore this email.</p>
|
||||
<p>Thanks,<br>The VC E-Commerce Team</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="background-color:#f4f4f4; padding:20px; text-align:center; color:#777777; font-size:12px;">
|
||||
© <%= new Date().getFullYear() %> VC E-Commerce. All rights reserved.
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</body>
|
||||
</html>
|
||||
BIN
structure.txt
Normal file
BIN
structure.txt
Normal file
Binary file not shown.
119
test-connections.js
Normal file
119
test-connections.js
Normal file
@@ -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);
|
||||
191
testWhatsApp.js
Normal file
191
testWhatsApp.js
Normal file
@@ -0,0 +1,191 @@
|
||||
// testWappConnect.js - COMPREHENSIVE TEST
|
||||
|
||||
require('dotenv').config();
|
||||
const axios = require('axios');
|
||||
|
||||
const BASE_URL = 'https://api.wappconnect.com';
|
||||
const SESSION = 'Sahasrara_metatech';
|
||||
const USERNAME = '8369805801';
|
||||
const PASSWORD = '918369805801';
|
||||
const TEST_PHONE = '918070905353';
|
||||
|
||||
async function comprehensiveTest() {
|
||||
console.log('🧪 WappConnect Comprehensive Test\n');
|
||||
console.log('📋 Configuration:');
|
||||
console.log(' Base URL:', BASE_URL);
|
||||
console.log(' Session:', SESSION);
|
||||
console.log(' Username:', USERNAME);
|
||||
console.log(' Test Phone:', TEST_PHONE);
|
||||
console.log('\n' + '='.repeat(60) + '\n');
|
||||
|
||||
// ===== TEST 1: Login =====
|
||||
console.log('TEST 1: Login to WappConnect\n');
|
||||
|
||||
let authToken = null;
|
||||
let cookies = null;
|
||||
|
||||
try {
|
||||
const loginResponse = await axios.post(
|
||||
`${BASE_URL}/api/auth/login`,
|
||||
{
|
||||
username: USERNAME,
|
||||
password: PASSWORD,
|
||||
},
|
||||
{
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
console.log('✅ Login successful!');
|
||||
console.log(' Response:', loginResponse.data);
|
||||
|
||||
if (loginResponse.data.token) {
|
||||
authToken = loginResponse.data.token;
|
||||
console.log(' Token:', authToken.substring(0, 20) + '...');
|
||||
}
|
||||
|
||||
if (loginResponse.headers['set-cookie']) {
|
||||
cookies = loginResponse.headers['set-cookie'].join('; ');
|
||||
console.log(' Cookies received');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('❌ Login failed:', error.message);
|
||||
if (error.response) {
|
||||
console.error(' Status:', error.response.status);
|
||||
console.error(' Data:', error.response.data);
|
||||
}
|
||||
}
|
||||
|
||||
console.log('\n' + '='.repeat(60) + '\n');
|
||||
|
||||
// ===== TEST 2: Check Session Status =====
|
||||
console.log('TEST 2: Check Session Status\n');
|
||||
|
||||
const statusEndpoints = [
|
||||
`/api/${SESSION}/status`,
|
||||
`/api/${SESSION}/check-connection`,
|
||||
`/api/${SESSION}/check-connection-session`,
|
||||
`/${SESSION}/status`,
|
||||
];
|
||||
|
||||
for (const endpoint of statusEndpoints) {
|
||||
try {
|
||||
console.log(`🔄 Trying: ${endpoint}`);
|
||||
|
||||
const headers = {
|
||||
'Content-Type': 'application/json',
|
||||
};
|
||||
|
||||
if (authToken) headers['Authorization'] = `Bearer ${authToken}`;
|
||||
if (cookies) headers['Cookie'] = cookies;
|
||||
|
||||
const response = await axios.get(`${BASE_URL}${endpoint}`, { headers });
|
||||
|
||||
console.log('✅ SUCCESS!');
|
||||
console.log(' Status:', response.data);
|
||||
break;
|
||||
} catch (error) {
|
||||
console.log(` ❌ Failed: ${error.response?.status || error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
console.log('\n' + '='.repeat(60) + '\n');
|
||||
|
||||
// ===== TEST 3: Send Message =====
|
||||
console.log('TEST 3: Send Test Message\n');
|
||||
|
||||
const messageEndpoints = [
|
||||
`/api/${SESSION}/send-text`,
|
||||
`/api/${SESSION}/sendText`,
|
||||
`/api/${SESSION}/send-message`,
|
||||
`/api/${SESSION}/sendMessage`,
|
||||
`/${SESSION}/send-text`,
|
||||
`/send-message/${SESSION}`,
|
||||
`/api/sendText/${SESSION}`,
|
||||
];
|
||||
|
||||
const messagePayloads = [
|
||||
// Payload variation 1
|
||||
{
|
||||
phone: TEST_PHONE,
|
||||
message: '🧪 Test from WappConnect - Method 1',
|
||||
isGroup: false,
|
||||
},
|
||||
// Payload variation 2
|
||||
{
|
||||
phone: TEST_PHONE,
|
||||
text: '🧪 Test from WappConnect - Method 2',
|
||||
},
|
||||
// Payload variation 3
|
||||
{
|
||||
chatId: `${TEST_PHONE}@c.us`,
|
||||
message: '🧪 Test from WappConnect - Method 3',
|
||||
},
|
||||
// Payload variation 4
|
||||
{
|
||||
number: TEST_PHONE,
|
||||
body: '🧪 Test from WappConnect - Method 4',
|
||||
},
|
||||
];
|
||||
|
||||
let messageSent = false;
|
||||
|
||||
for (const endpoint of messageEndpoints) {
|
||||
if (messageSent) break;
|
||||
|
||||
for (const payload of messagePayloads) {
|
||||
try {
|
||||
console.log(`🔄 Trying: ${endpoint}`);
|
||||
console.log(` Payload:`, JSON.stringify(payload, null, 2));
|
||||
|
||||
const headers = {
|
||||
'Content-Type': 'application/json',
|
||||
};
|
||||
|
||||
if (authToken) headers['Authorization'] = `Bearer ${authToken}`;
|
||||
if (cookies) headers['Cookie'] = cookies;
|
||||
|
||||
const response = await axios.post(
|
||||
`${BASE_URL}${endpoint}`,
|
||||
payload,
|
||||
{ headers, timeout: 15000 }
|
||||
);
|
||||
|
||||
console.log('✅ MESSAGE SENT SUCCESSFULLY!');
|
||||
console.log(' Response:', response.data);
|
||||
console.log('\n📱 CHECK YOUR WHATSAPP NOW!\n');
|
||||
|
||||
messageSent = true;
|
||||
|
||||
// Save working configuration
|
||||
console.log('💾 WORKING CONFIGURATION:');
|
||||
console.log(' Endpoint:', endpoint);
|
||||
console.log(' Payload:', JSON.stringify(payload, null, 2));
|
||||
if (authToken) console.log(' Uses Token: Yes');
|
||||
if (cookies) console.log(' Uses Cookies: Yes');
|
||||
|
||||
break;
|
||||
} catch (error) {
|
||||
console.log(` ❌ Failed: ${error.response?.status || error.message}`);
|
||||
if (error.response?.data) {
|
||||
console.log(` Error:`, error.response.data);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!messageSent) {
|
||||
console.log('\n❌ Could not send message with any endpoint/payload combination');
|
||||
console.log('\nℹ️ Recommendations:');
|
||||
console.log(' 1. Check if WhatsApp is connected in dashboard');
|
||||
console.log(' 2. Try connecting WhatsApp again (scan QR)');
|
||||
console.log(' 3. Contact WappConnect support');
|
||||
console.log(' 4. Consider using Twilio instead (easier setup)');
|
||||
}
|
||||
|
||||
console.log('\n' + '='.repeat(60));
|
||||
}
|
||||
|
||||
comprehensiveTest();
|
||||
Reference in New Issue
Block a user