first commit

This commit is contained in:
2026-03-10 12:43:27 +05:30
commit edb525eb80
79 changed files with 25644 additions and 0 deletions

4
.dockerignore Normal file
View File

@@ -0,0 +1,4 @@
node_modules
npm-debug.log
.git
.env

19
.env.example Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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

File diff suppressed because it is too large Load Diff

69
package.json Normal file
View 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
View 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
View 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
View 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
View 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,
};

View 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
View 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;

View 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',
});
}
};

View 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);
}
};

View 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,
},
};
}

View 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);
}
};

View 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);
}
};

File diff suppressed because it is too large Load Diff

View 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);
}
};

View 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',
});
}
};

View 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;
}

View 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 };

View 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;
}

View 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);
}
};

View 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' });
}
};

View 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,
};

View 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);
}
};

View 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);
}
};

View 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);
}
};

View 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);
}
};

View 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);
}
};

View 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);
}
};

View 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",
});
}
};

View 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);
}
};

View 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);
}
};

View 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);
}
};

View 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);
}
};

View 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
View 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,
};

View 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
View File

@@ -0,0 +1,10 @@
const multer = require("multer");
const upload = multer({
storage: multer.memoryStorage(),
limits: { fileSize: 5 * 1024 * 1024 },
});
module.exports = upload;

View 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 };

View 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);

View 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
View 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
View 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;

View 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;

View 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
View 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;

View 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
View 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
View 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;

View 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
View 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
View 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;

View 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();

View 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
View 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
View 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();

View 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,
};

View 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
View 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 };

View 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}`;
};

View 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
View 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
View 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
View 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
View 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 };

View 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;">
&copy; <%= new Date().getFullYear() %> VC E-Commerce. All rights reserved.
</td>
</tr>
</table>
</body>
</html>

View 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;">
&copy; <%= new Date().getFullYear() %> VC E-Commerce. All rights reserved.
</td>
</tr>
</table>
</body>
</html>

BIN
structure.txt Normal file

Binary file not shown.

119
test-connections.js Normal file
View 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
View 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();