tables added based on the frontend ui and backend is up need to test thouroughly

This commit is contained in:
laxmanhalaki 2026-01-20 19:42:37 +05:30
commit 8984a314a7
100 changed files with 24805 additions and 0 deletions

137
.gitignore vendored Normal file
View File

@ -0,0 +1,137 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
*.lcov
# nyc test coverage
.nyc_output
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# TypeScript v1 declaration files
typings/
# 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
.env.local
.env.development.local
.env.test.local
.env.production.local
# parcel-bundler cache (https://parceljs.org/)
.cache
.parcel-cache
# Next.js build output
.next
# Nuxt.js build / generate output
.nuxt
dist
# Gatsby files
.cache/
# Comment in the public line in if your project uses Gatsby and not Next.js
# https://nextjs.org/blog/next-9-1#public-directory-support
# public
# vuepress build output
.vuepress/dist
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
# TernJS port file
.tern-port
# Stores VSCode versions used for testing VSCode extensions
.vscode-test
# yarn v2
.yarn/cache
.yarn/unplugged
.yarn/build-state.yml
.yarn/install-state.gz
.pnp.*
# IDE
.vscode/
.idea/
*.swp
*.swo
# OS
.DS_Store
Thumbs.db
# Uploads
uploads/
!uploads/.gitkeep
# Database
*.sqlite
*.db
# GCP Service Account Key
config/gcp-key.json

View File

@ -0,0 +1,464 @@
# Instructions for AI Assistant (Cursor/Windsurf/Cline/etc.)
## 🎯 Objective
Complete the Royal Enfield Dealership Onboarding System backend by creating all remaining files based on the comprehensive documentation and examples provided.
## 📚 Documentation Files to Read First
1. **`back.md`** - MOST IMPORTANT - Complete architecture, API endpoints, database schema
2. **`README.md`** - Quick start guide and file structure overview
3. **`config/constants.js`** - All system constants, roles, statuses
4. **`server.js`** - Main entry point and setup
5. **Example files:**
- `models/User.js` and `models/Outlet.js` - Model pattern
- `models/Resignation.js` - Complete resignation model
- `controllers/resignationController.js` - Complete controller with business logic
- `routes/resignations.js` - Route pattern
- `middleware/auth.js` - Authentication middleware
- `middleware/roleCheck.js` - Authorization middleware
## 📋 Files Already Created ✅
### Core Files
- ✅ package.json
- ✅ .env.example
- ✅ server.js
- ✅ .gitignore
- ✅ README.md
- ✅ back.md
- ✅ AI-ASSISTANT-INSTRUCTIONS.md (this file)
### Config
- ✅ config/database.js
- ✅ config/email.js
- ✅ config/constants.js
### Models
- ✅ models/index.js
- ✅ models/User.js
- ✅ models/Outlet.js
- ✅ models/Resignation.js
### Controllers
- ✅ controllers/resignationController.js
### Routes
- ✅ routes/resignations.js
### Middleware
- ✅ middleware/auth.js
- ✅ middleware/roleCheck.js
- ✅ middleware/errorHandler.js
### Utils
- ✅ utils/logger.js
## 🚧 Files YOU Need to Create
### Models (Follow pattern in models/Resignation.js)
**models/Application.js**
- See schema in back.md under "applications" table
- Fields: id, applicationId, applicantName, email, phone, businessType, proposedLocation, city, state, pincode, currentStage, status, ranking, submittedBy, submittedAt, progressPercentage, documents, timeline
- Associations: belongsTo User (submittedBy), hasMany FinancePayment, hasMany Worknote
**models/ConstitutionalChange.js**
- See schema in back.md under "constitutional_changes" table
- Fields: id, requestId, outletId, dealerId, changeType, currentStructure, proposedStructure, reason, effectiveDate, currentStage, status, progressPercentage, submittedOn, documents, timeline
- Associations: belongsTo Outlet, belongsTo User (dealer), hasMany Worknote
**models/RelocationRequest.js**
- See schema in back.md under "relocation_requests" table
- Fields: id, requestId, outletId, dealerId, relocationType, currentAddress, currentLatitude, currentLongitude, proposedAddress, proposedLatitude, proposedLongitude, proposedCity, proposedState, proposedPincode, distance, reason, effectiveDate, currentStage, status, progressPercentage, submittedOn, documents, timeline
- Associations: belongsTo Outlet, belongsTo User (dealer), hasMany Worknote
**models/Worknote.js**
- See schema in back.md under "worknotes" table
- Fields: id, requestId, requestType (ENUM: application, resignation, constitutional, relocation), userId, userName, userRole, message, attachments, timestamp
- Associations: belongsTo User
**models/Document.js**
- See schema in back.md under "documents" table
- Fields: id, filename, originalName, mimeType, size, path, uploadedBy, relatedTo, relatedId, documentType, uploadedAt
- Associations: belongsTo User (uploadedBy)
**models/AuditLog.js**
- See schema in back.md under "audit_logs" table
- Fields: id, userId, userName, userRole, action (ENUM from AUDIT_ACTIONS), entityType, entityId, changes (JSON), ipAddress, timestamp
- Associations: belongsTo User
**models/FinancePayment.js**
- See schema in back.md under "finance_payments" table
- Fields: id, applicationId, outletId, dealerId, paymentType, amount, dueDate, paidDate, status, transactionId, paymentMode, remarks
- Associations: belongsTo Application, belongsTo Outlet, belongsTo User (dealer)
**models/FnF.js**
- See schema in back.md under "fnf_settlements" table
- Fields: id, fnfId, resignationId, outletId, dealerId, totalDues, securityDepositRefund, otherCharges, netSettlement, status, settledDate, progressPercentage
- Associations: belongsTo Resignation, belongsTo Outlet, belongsTo User (dealer)
**models/Region.js**
- See schema in back.md under "regions" table
- Fields: id, name (ENUM: East, West, North, South, Central), code, headName, headEmail, isActive
- Associations: hasMany Zone
**models/Zone.js**
- See schema in back.md under "zones" table
- Fields: id, name, code, regionId, managerName, managerEmail, states (JSON array), isActive
- Associations: belongsTo Region
### Controllers (Follow pattern in controllers/resignationController.js)
**controllers/authController.js**
- login(req, res, next) - Validate credentials, generate JWT token, return user info
- logout(req, res, next) - Invalidate token (if using token blacklist)
- getMe(req, res, next) - Get current user info from token
- refreshToken(req, res, next) - Refresh expired token
**controllers/userController.js**
- getAllUsers(req, res, next) - Get all users (Super Admin only)
- getUserById(req, res, next) - Get specific user
- createUser(req, res, next) - Create new user
- updateUser(req, res, next) - Update user info
- deleteUser(req, res, next) - Deactivate user
- changePassword(req, res, next) - Change password
**controllers/applicationController.js**
- createApplication(req, res, next) - Public endpoint for dealer application
- getApplications(req, res, next) - Get applications (role-based filtering)
- getApplicationById(req, res, next) - Get application details
- assignRanking(req, res, next) - DD assigns ranking
- approveApplication(req, res, next) - Move to next stage
- rejectApplication(req, res, next) - Reject application
**controllers/constitutionalController.js**
- createConstitutionalChange(req, res, next) - Dealer creates request
- getConstitutionalChanges(req, res, next) - List requests
- getConstitutionalChangeById(req, res, next) - Get details
- approveConstitutionalChange(req, res, next) - Approve and move stage
- rejectConstitutionalChange(req, res, next) - Reject request
**controllers/relocationController.js**
- createRelocation(req, res, next) - Dealer creates request
- getRelocations(req, res, next) - List requests
- getRelocationById(req, res, next) - Get details
- calculateDistance(req, res, next) - Calculate distance between locations
- approveRelocation(req, res, next) - Approve and move stage
- rejectRelocation(req, res, next) - Reject request
**controllers/outletController.js**
- getMyOutlets(req, res, next) - Dealer gets their outlets
- getAllOutlets(req, res, next) - Get all outlets (Admin)
- getOutletById(req, res, next) - Get outlet details
- createOutlet(req, res, next) - Create new outlet (Admin)
- updateOutlet(req, res, next) - Update outlet info
**controllers/worknoteController.js**
- createWorknote(req, res, next) - Add worknote to any request
- getWorknotes(req, res, next) - Get worknotes for a request
- deleteWorknote(req, res, next) - Delete own worknote (if needed)
**controllers/financeController.js**
- getOnboardingPayments(req, res, next) - Get pending payments for onboarding
- getPaymentById(req, res, next) - Get payment details
- markPaymentPaid(req, res, next) - Mark payment as received
- getFnFList(req, res, next) - Get F&F settlement requests
- getFnFById(req, res, next) - Get F&F details
- processFnF(req, res, next) - Process F&F settlement
**controllers/masterController.js**
- getRegions(req, res, next) - Get all regions
- createRegion(req, res, next) - Create region (Super Admin)
- updateRegion(req, res, next) - Update region
- getZones(req, res, next) - Get zones (optionally by region)
- createZone(req, res, next) - Create zone
- updateZone(req, res, next) - Update zone
**controllers/dashboardController.js**
- getStats(req, res, next) - Get dashboard stats (role-based)
- getRecentActivity(req, res, next) - Get recent activities
### Routes (Follow pattern in routes/resignations.js)
Create route files for all controllers:
- routes/auth.js
- routes/users.js
- routes/applications.js
- routes/constitutional.js
- routes/relocations.js
- routes/outlets.js
- routes/worknotes.js
- routes/finance.js
- routes/master.js
- routes/dashboard.js
Each route file should:
1. Import required controller
2. Import auth and roleCheck middleware
3. Define routes with appropriate HTTP methods
4. Apply auth middleware to protected routes
5. Apply roleCheck middleware with correct roles (see back.md permission matrix)
### Middleware
**middleware/upload.js**
- Configure multer for file uploads
- File type validation (PDF, images only)
- File size limit (10MB)
- Storage configuration (disk or cloud)
**middleware/validation.js**
- Validation functions using express-validator
- Examples: validateRegistration, validateLogin, validateResignation, etc.
### Utilities
**utils/emailService.js**
- sendEmail(to, subject, html) - Send email using nodemailer
- sendApplicationConfirmation(application)
- sendApprovalNotification(application, stage)
- sendRejectionNotification(application, reason)
- sendDeadlineReminder(application)
- Use config/email.js for SMTP configuration
**utils/emailTemplates.js**
- HTML templates for all email types
- applicationConfirmationTemplate(data)
- approvalNotificationTemplate(data)
- rejectionNotificationTemplate(data)
- stageProgressionTemplate(data)
- deadlineReminderTemplate(data)
**utils/helpers.js**
- calculateDistance(lat1, lon1, lat2, lon2) - Haversine formula for distance
- generateUniqueId(prefix, count) - Generate unique IDs
- formatDate(date) - Date formatting helper
- validatePincode(pincode) - Indian pincode validation
### Migrations
**migrations/initial-setup.js**
- Create all tables using Sequelize sync
- Seed initial data:
- Super Admin user (email: admin@royalenfield.com, password: Admin@123)
- 5 Regions (East, West, North, South, Central)
- Sample zones for each region
- Sample users for each role (for testing)
- Sample outlets for dealer users
## 🎨 Code Patterns to Follow
### Model Pattern
```javascript
const { CONSTANT } = require('../config/constants');
module.exports = (sequelize, DataTypes) => {
const ModelName = sequelize.define('ModelName', {
id: {
type: DataTypes.UUID,
defaultValue: DataTypes.UUIDV4,
primaryKey: true
},
// ... other fields from schema in back.md
}, {
tableName: 'table_name',
timestamps: true,
indexes: [/* ... */]
});
ModelName.associate = (models) => {
// Define relationships
};
return ModelName;
};
```
### Controller Pattern
```javascript
const db = require('../models');
const logger = require('../utils/logger');
const { CONSTANT } = require('../config/constants');
exports.functionName = async (req, res, next) => {
try {
// Business logic here
// Log action
logger.info('Action performed', { user: req.user.email });
// Return response
res.json({
success: true,
data: result
});
} catch (error) {
logger.error('Error:', error);
next(error);
}
};
```
### Route Pattern
```javascript
const express = require('express');
const router = express.Router();
const controller = require('../controllers/controllerName');
const auth = require('../middleware/auth');
const roleCheck = require('../middleware/roleCheck');
const { ROLES } = require('../config/constants');
router.post('/create', auth, roleCheck([ROLES.ROLE_NAME]), controller.create);
router.get('/list', auth, controller.getList);
router.get('/:id', auth, controller.getById);
module.exports = router;
```
## 🔒 Security Checklist
For each controller function:
- [ ] Use try-catch blocks
- [ ] Validate input data
- [ ] Check user permissions
- [ ] Use database transactions for multi-step operations
- [ ] Log actions to AuditLog
- [ ] Sanitize user input
- [ ] Return appropriate HTTP status codes
## 🧪 Testing Approach
After creating files:
1. Run `npm install` to install dependencies
2. Set up PostgreSQL database
3. Configure .env file
4. Run migrations: `npm run migrate`
5. Start server: `npm run dev`
6. Test endpoints using Postman/Thunder Client
7. Test each role's permissions
8. Verify database constraints
## 📊 Database Relationships Summary
- **User** → hasMany → Application, Outlet, Resignation, ConstitutionalChange, RelocationRequest
- **Outlet** → belongsTo → User (dealer)
- **Outlet** → hasMany → Resignation, ConstitutionalChange, RelocationRequest
- **Resignation** → belongsTo → Outlet, User (dealer)
- **ConstitutionalChange** → belongsTo → Outlet, User (dealer)
- **RelocationRequest** → belongsTo → Outlet, User (dealer)
- **FinancePayment** → belongsTo → Application, Outlet, User (dealer)
- **FnF** → belongsTo → Resignation, Outlet, User (dealer)
- **Region** → hasMany → Zone
- **Zone** → belongsTo → Region
- **Worknote** → belongsTo → User
- **Document** → belongsTo → User
- **AuditLog** → belongsTo → User
## 🚀 Priority Order for File Creation
1. **Models first** (all remaining models)
2. **Middleware** (upload, validation)
3. **Utils** (emailService, emailTemplates, helpers)
4. **Controllers** (in order: auth, outlets, constitutional, relocation, applications, finance, master, dashboard)
5. **Routes** (for each controller)
6. **Migrations** (initial-setup.js)
## 💡 Tips
- Use the resignation module as a complete reference - it has everything you need to understand the pattern
- All API endpoints are documented in back.md with request/response examples
- Database schema is fully documented in back.md
- Permission matrix in back.md shows which roles can access which endpoints
- Constants file has all ENUMs you need
- Use logger.info/error for all important actions
- Always create audit logs for create/update/delete/approve/reject actions
- Use database transactions for multi-step operations
- Send email notifications after important actions (use TODO comments for now)
## 📞 Questions to Ask Yourself
Before creating each file:
1. What is this file's purpose? (Check back.md)
2. What data does it work with? (Check database schema)
3. Who can access it? (Check permission matrix)
4. What validations are needed? (Check API documentation)
5. What side effects should happen? (Audit logs, emails, status updates)
## ✅ Success Criteria
Backend is complete when:
- [ ] All model files created and associations defined
- [ ] All controller files created with business logic
- [ ] All route files created with proper middleware
- [ ] All middleware files created
- [ ] All utility files created
- [ ] Migration file creates schema and seeds data
- [ ] Server starts without errors
- [ ] All API endpoints respond correctly
- [ ] Role-based permissions work correctly
- [ ] Database relationships are correct
- [ ] Authentication/Authorization works
- [ ] File uploads work
- [ ] Email service is configured (can use console.log for testing)
## 🎯 Final Goal
A fully functional Node.js + Express + PostgreSQL backend that:
1. ✅ Authenticates users with JWT
2. ✅ Enforces role-based permissions
3. ✅ Handles all CRUD operations for all entities
4. ✅ Manages multi-stage workflows
5. ✅ Tracks audit trails
6. ✅ Sends email notifications
7. ✅ Handles file uploads
8. ✅ Provides role-based dashboard data
9. ✅ Ready to connect to the existing frontend
10. ✅ Ready for production deployment
---
**Good luck! The backend structure is solid, patterns are clear, and documentation is comprehensive. You have everything you need to complete this successfully!** 🚀

238
README.md Normal file
View File

@ -0,0 +1,238 @@
# Royal Enfield Dealership Onboarding System - Backend
## 🚀 Quick Start
### 1. Install Dependencies
```bash
npm install
```
### 2. Setup Database
Create PostgreSQL database:
```bash
createdb royal_enfield_onboarding
```
### 3. Configure Environment
```bash
cp .env.example .env
# Edit .env with your database credentials and other settings
```
### 4. Run Migrations
```bash
npm run migrate
```
### 5. Start Server
```bash
# Development
npm run dev
# Production
npm start
```
## 📁 Complete File Structure
This backend requires the following files to be fully functional. Files marked with ✅ are already created. Files marked with ⚠️ need to be created by your AI assistant:
### Root Files
- ✅ `package.json`
- ✅ `.env.example`
- ✅ `server.js`
- ✅ `README.md`
- ✅ `back.md` (Comprehensive documentation)
- ⚠️ `.gitignore`
### Config Files (/config)
- ✅ `database.js`
- ✅ `email.js`
- ✅ `constants.js`
### Models (/models)
- ✅ `index.js`
- ✅ `User.js`
- ✅ `Outlet.js`
- ⚠️ `Application.js` - Dealer application model
- ⚠️ `Resignation.js` - Resignation request model
- ⚠️ `ConstitutionalChange.js` - Constitutional change model
- ⚠️ `RelocationRequest.js` - Relocation request model
- ⚠️ `Worknote.js` - Discussion worknotes model
- ⚠️ `Document.js` - Document uploads model
- ⚠️ `AuditLog.js` - Audit trail model
- ⚠️ `FinancePayment.js` - Finance payment tracking
- ⚠️ `FnF.js` - Full & Final settlement
- ⚠️ `Region.js` - Regional hierarchy
- ⚠️ `Zone.js` - Zone model
### Controllers (/controllers)
- ⚠️ `authController.js` - Login, logout, token refresh
- ⚠️ `userController.js` - User management
- ⚠️ `applicationController.js` - Dealer applications
- ⚠️ `resignationController.js` - Resignation workflow
- ⚠️ `constitutionalController.js` - Constitutional changes
- ⚠️ `relocationController.js` - Relocation requests
- ⚠️ `outletController.js` - Outlet management
- ⚠️ `worknoteController.js` - Discussion platform
- ⚠️ `financeController.js` - Finance operations
- ⚠️ `masterController.js` - Master configuration
- ⚠️ `dashboardController.js` - Dashboard statistics
### Routes (/routes)
- ⚠️ `auth.js` - Authentication routes
- ⚠️ `users.js` - User routes
- ⚠️ `applications.js` - Application routes
- ⚠️ `resignations.js` - Resignation routes
- ⚠️ `constitutional.js` - Constitutional change routes
- ⚠️ `relocations.js` - Relocation routes
- ⚠️ `outlets.js` - Outlet routes
- ⚠️ `worknotes.js` - Worknote routes
- ⚠️ `finance.js` - Finance routes
- ⚠️ `master.js` - Master configuration routes
- ⚠️ `dashboard.js` - Dashboard routes
### Middleware (/middleware)
- ⚠️ `auth.js` - JWT verification middleware
- ⚠️ `roleCheck.js` - Role-based access control
- ⚠️ `upload.js` - File upload middleware
- ⚠️ `validation.js` - Input validation
- ⚠️ `errorHandler.js` - Global error handler
### Utilities (/utils)
- ⚠️ `emailTemplates.js` - Email HTML templates
- ⚠️ `emailService.js` - Email sending utilities
- ⚠️ `logger.js` - Winston logger
- ⚠️ `helpers.js` - Helper functions
### Migrations (/migrations)
- ⚠️ `initial-setup.js` - Initial database schema and seed data
## 📝 Instructions for AI Assistant
**To complete this backend, your AI IDE should:**
1. Read the `back.md` file for complete architecture understanding
2. Create all remaining model files based on the database schema in `back.md`
3. Create all controller files with business logic as described in API endpoints
4. Create all route files mapping HTTP requests to controllers
5. Create middleware files for authentication, authorization, and validation
6. Create utility files for email, logging, and helpers
7. Create migration file to set up initial database schema and seed data
**Each file should follow these patterns:**
### Model Pattern:
```javascript
const { CONSTANTS } = require('../config/constants');
module.exports = (sequelize, DataTypes) => {
const ModelName = sequelize.define('ModelName', {
// Fields defined in back.md schema
});
ModelName.associate = (models) => {
// Relationships defined in back.md
};
return ModelName;
};
```
### Controller Pattern:
```javascript
const db = require('../models');
const logger = require('../utils/logger');
exports.functionName = async (req, res, next) => {
try {
// Business logic
res.json({ success: true, data: result });
} catch (error) {
logger.error('Error:', error);
next(error);
}
};
```
### Route Pattern:
```javascript
const express = require('express');
const router = express.Router();
const controller = require('../controllers/controllerName');
const auth = require('../middleware/auth');
const roleCheck = require('../middleware/roleCheck');
router.get('/', auth, roleCheck(['Role1', 'Role2']), controller.list);
router.post('/', auth, roleCheck(['Role1']), controller.create);
module.exports = router;
```
## 🔌 API Documentation
Full API documentation is available in `back.md` including:
- All endpoint URLs
- Request/Response formats
- Authentication requirements
- Role permissions
- Example payloads
## 🗄️ Database Schema
Complete database schema with all tables, columns, relationships, and indexes is documented in `back.md`.
## 🔐 Security
The backend implements:
- JWT authentication
- Role-based access control
- Password hashing with bcrypt
- Input validation
- SQL injection prevention
- Rate limiting
- CORS protection
- Security headers (Helmet)
## 📧 Email Notifications
Email templates and triggers are defined in `utils/emailTemplates.js` and `utils/emailService.js`.
## 📊 Logging
Winston logger is configured in `utils/logger.js` with multiple transports.
## 🧪 Testing
Run tests with:
```bash
npm test
npm run test:coverage
```
## 🚢 Deployment
See `back.md` for detailed deployment instructions for:
- Railway
- Render
- AWS/DigitalOcean
- Docker
## 📞 Support
Refer to `back.md` for:
- Troubleshooting common issues
- Database backup/restore
- Maintenance tasks
- Complete architecture documentation
---
**Next Steps:**
1. Have your AI assistant read `back.md`
2. Ask it to create all remaining files following the patterns above
3. Test API endpoints
4. Connect frontend to backend
5. Deploy to production
Good luck! 🚀

1270
back.md Normal file

File diff suppressed because it is too large Load Diff

34
config/auth.js Normal file
View File

@ -0,0 +1,34 @@
const jwt = require('jsonwebtoken');
const JWT_SECRET = process.env.JWT_SECRET || 'your-secret-key-change-in-production';
const JWT_EXPIRE = process.env.JWT_EXPIRE || '7d';
// Generate JWT token
const generateToken = (user) => {
const payload = {
userId: user.id,
email: user.email,
role: user.role,
region: user.region,
zone: user.zone
};
return jwt.sign(payload, JWT_SECRET, {
expiresIn: JWT_EXPIRE
});
};
// Verify JWT token
const verifyToken = (token) => {
try {
return jwt.verify(token, JWT_SECRET);
} catch (error) {
throw new Error('Invalid or expired token');
}
};
module.exports = {
generateToken,
verifyToken,
JWT_SECRET
};

209
config/constants.js Normal file
View File

@ -0,0 +1,209 @@
// User Roles
const ROLES = {
DD: 'DD',
DD_ZM: 'DD-ZM',
RBM: 'RBM',
ZBH: 'ZBH',
DD_LEAD: 'DD Lead',
DD_HEAD: 'DD Head',
NBH: 'NBH',
DD_ADMIN: 'DD Admin',
LEGAL_ADMIN: 'Legal Admin',
SUPER_ADMIN: 'Super Admin',
DD_AM: 'DD AM',
FINANCE: 'Finance',
DEALER: 'Dealer'
};
// Regions
const REGIONS = {
EAST: 'East',
WEST: 'West',
NORTH: 'North',
SOUTH: 'South',
CENTRAL: 'Central'
};
// Application Stages
const APPLICATION_STAGES = {
DD: 'DD',
DD_ZM: 'DD-ZM',
RBM: 'RBM',
ZBH: 'ZBH',
DD_LEAD: 'DD Lead',
DD_HEAD: 'DD Head',
NBH: 'NBH',
LEGAL: 'Legal',
FINANCE: 'Finance',
APPROVED: 'Approved',
REJECTED: 'Rejected'
};
// Application Status
const APPLICATION_STATUS = {
PENDING: 'Pending',
IN_REVIEW: 'In Review',
APPROVED: 'Approved',
REJECTED: 'Rejected'
};
// Resignation Stages
const RESIGNATION_STAGES = {
ASM: 'ASM',
RBM: 'RBM',
ZBH: 'ZBH',
NBH: 'NBH',
DD_ADMIN: 'DD Admin',
LEGAL: 'Legal',
FINANCE: 'Finance',
FNF_INITIATED: 'F&F Initiated',
COMPLETED: 'Completed',
REJECTED: 'Rejected'
};
// Resignation Types
const RESIGNATION_TYPES = {
VOLUNTARY: 'Voluntary',
RETIREMENT: 'Retirement',
HEALTH_ISSUES: 'Health Issues',
BUSINESS_CLOSURE: 'Business Closure',
OTHER: 'Other'
};
// Constitutional Change Types
const CONSTITUTIONAL_CHANGE_TYPES = {
OWNERSHIP_TRANSFER: 'Ownership Transfer',
PARTNERSHIP_CHANGE: 'Partnership Change',
LLP_CONVERSION: 'LLP Conversion',
COMPANY_FORMATION: 'Company Formation',
DIRECTOR_CHANGE: 'Director Change'
};
// Constitutional Change Stages
const CONSTITUTIONAL_STAGES = {
DD_ADMIN_REVIEW: 'DD Admin Review',
LEGAL_REVIEW: 'Legal Review',
NBH_APPROVAL: 'NBH Approval',
FINANCE_CLEARANCE: 'Finance Clearance',
COMPLETED: 'Completed',
REJECTED: 'Rejected'
};
// Relocation Types
const RELOCATION_TYPES = {
WITHIN_CITY: 'Within City',
INTERCITY: 'Intercity',
INTERSTATE: 'Interstate'
};
// Relocation Stages
const RELOCATION_STAGES = {
DD_ADMIN_REVIEW: 'DD Admin Review',
RBM_REVIEW: 'RBM Review',
NBH_APPROVAL: 'NBH Approval',
LEGAL_CLEARANCE: 'Legal Clearance',
COMPLETED: 'Completed',
REJECTED: 'Rejected'
};
// Outlet Types
const OUTLET_TYPES = {
DEALERSHIP: 'Dealership',
STUDIO: 'Studio'
};
// Outlet Status
const OUTLET_STATUS = {
ACTIVE: 'Active',
PENDING_RESIGNATION: 'Pending Resignation',
CLOSED: 'Closed'
};
// Business Types
const BUSINESS_TYPES = {
DEALERSHIP: 'Dealership',
STUDIO: 'Studio'
};
// Payment Types
const PAYMENT_TYPES = {
SECURITY_DEPOSIT: 'Security Deposit',
LICENSE_FEE: 'License Fee',
SETUP_FEE: 'Setup Fee',
OTHER: 'Other'
};
// Payment Status
const PAYMENT_STATUS = {
PENDING: 'Pending',
PAID: 'Paid',
OVERDUE: 'Overdue',
WAIVED: 'Waived'
};
// F&F Status
const FNF_STATUS = {
INITIATED: 'Initiated',
DD_CLEARANCE: 'DD Clearance',
LEGAL_CLEARANCE: 'Legal Clearance',
FINANCE_APPROVAL: 'Finance Approval',
COMPLETED: 'Completed'
};
// Audit Actions
const AUDIT_ACTIONS = {
CREATED: 'CREATED',
UPDATED: 'UPDATED',
APPROVED: 'APPROVED',
REJECTED: 'REJECTED',
DELETED: 'DELETED',
STAGE_CHANGED: 'STAGE_CHANGED',
DOCUMENT_UPLOADED: 'DOCUMENT_UPLOADED',
WORKNOTE_ADDED: 'WORKNOTE_ADDED'
};
// Document Types
const DOCUMENT_TYPES = {
GST_CERTIFICATE: 'GST Certificate',
PAN_CARD: 'PAN Card',
AADHAAR: 'Aadhaar',
PARTNERSHIP_DEED: 'Partnership Deed',
LLP_AGREEMENT: 'LLP Agreement',
INCORPORATION_CERTIFICATE: 'Certificate of Incorporation',
MOA: 'MOA',
AOA: 'AOA',
BOARD_RESOLUTION: 'Board Resolution',
PROPERTY_DOCUMENTS: 'Property Documents',
BANK_STATEMENT: 'Bank Statement',
OTHER: 'Other'
};
// Request Types
const REQUEST_TYPES = {
APPLICATION: 'application',
RESIGNATION: 'resignation',
CONSTITUTIONAL: 'constitutional',
RELOCATION: 'relocation'
};
module.exports = {
ROLES,
REGIONS,
APPLICATION_STAGES,
APPLICATION_STATUS,
RESIGNATION_STAGES,
RESIGNATION_TYPES,
CONSTITUTIONAL_CHANGE_TYPES,
CONSTITUTIONAL_STAGES,
RELOCATION_TYPES,
RELOCATION_STAGES,
OUTLET_TYPES,
OUTLET_STATUS,
BUSINESS_TYPES,
PAYMENT_TYPES,
PAYMENT_STATUS,
FNF_STATUS,
AUDIT_ACTIONS,
DOCUMENT_TYPES,
REQUEST_TYPES
};

49
config/database.js Normal file
View File

@ -0,0 +1,49 @@
require('dotenv').config();
module.exports = {
development: {
username: process.env.DB_USER || 'laxman',
password: process.env.DB_PASSWORD || 'Admin@123',
database: process.env.DB_NAME || 'royal_enfield_onboarding',
host: process.env.DB_HOST || 'localhost',
port: process.env.DB_PORT || 5432,
dialect: 'postgres',
logging: console.log,
pool: {
max: 5,
min: 0,
acquire: 30000,
idle: 10000
}
},
production: {
username: process.env.DB_USER || 'laxman',
password: process.env.DB_PASSWORD || 'Admin@123',
database: process.env.DB_NAME || 'royal_enfield_onboarding',
host: process.env.DB_HOST || 'localhost',
port: process.env.DB_PORT || 5432,
dialect: 'postgres',
logging: false,
pool: {
max: 20,
min: 5,
acquire: 60000,
idle: 10000
},
dialectOptions: {
ssl: process.env.DB_SSL === 'true' ? {
require: true,
rejectUnauthorized: false
} : false
}
},
test: {
username: process.env.DB_USER || 'laxman',
password: process.env.DB_PASSWORD || 'Admin@123',
database: process.env.DB_NAME + '_test' || 'royal_enfield_onboarding_test',
host: process.env.DB_HOST || 'localhost',
port: process.env.DB_PORT || 5432,
dialect: 'postgres',
logging: false
}
};

12
config/email.js Normal file
View File

@ -0,0 +1,12 @@
require('dotenv').config();
module.exports = {
host: process.env.EMAIL_HOST || 'smtp.gmail.com',
port: parseInt(process.env.EMAIL_PORT) || 587,
secure: process.env.EMAIL_SECURE === 'true',
auth: {
user: process.env.EMAIL_USER,
pass: process.env.EMAIL_PASSWORD
},
from: process.env.EMAIL_FROM || 'Royal Enfield <noreply@royalenfield.com>'
};

View File

@ -0,0 +1,135 @@
const { Application } = require('../models');
const { v4: uuidv4 } = require('uuid');
exports.submitApplication = async (req, res) => {
try {
const {
applicantName, email, phone, businessType, locationType,
preferredLocation, city, state, experienceYears, investmentCapacity
} = req.body;
const applicationId = `APP-${new Date().getFullYear()}-${uuidv4().substring(0, 6).toUpperCase()}`;
const application = await Application.create({
applicationId,
applicantName,
email,
phone,
businessType,
preferredLocation,
city,
state,
experienceYears,
investmentCapacity,
currentStage: 'DD',
overallStatus: 'Pending',
progressPercentage: 0
});
res.status(201).json({
success: true,
message: 'Application submitted successfully',
applicationId: application.applicationId
});
} catch (error) {
console.error('Submit application error:', error);
res.status(500).json({ success: false, message: 'Error submitting application' });
}
};
exports.getApplications = async (req, res) => {
try {
const applications = await Application.findAll({
order: [['createdAt', 'DESC']]
});
res.json({ success: true, applications });
} catch (error) {
console.error('Get applications error:', error);
res.status(500).json({ success: false, message: 'Error fetching applications' });
}
};
exports.getApplicationById = async (req, res) => {
try {
const { id } = req.params;
const application = await Application.findOne({
where: {
[Op.or]: [
{ id },
{ applicationId: id }
]
}
});
if (!application) {
return res.status(404).json({ success: false, message: 'Application not found' });
}
res.json({ success: true, application });
} catch (error) {
console.error('Get application error:', error);
res.status(500).json({ success: false, message: 'Error fetching application' });
}
};
exports.takeAction = async (req, res) => {
try {
const { id } = req.params;
const { action, comments, rating } = req.body;
const application = await Application.findOne({
where: {
[Op.or]: [
{ id },
{ applicationId: id }
]
}
});
if (!application) {
return res.status(404).json({ success: false, message: 'Application not found' });
}
await application.update({
overallStatus: action,
updatedAt: new Date()
});
res.json({ success: true, message: 'Action taken successfully' });
} catch (error) {
console.error('Take action error:', error);
res.status(500).json({ success: false, message: 'Error processing action' });
}
};
exports.uploadDocuments = async (req, res) => {
try {
const { id } = req.params;
const { documents } = req.body;
const application = await Application.findOne({
where: {
[Op.or]: [
{ id },
{ applicationId: id }
]
}
});
if (!application) {
return res.status(404).json({ success: false, message: 'Application not found' });
}
await application.update({
documents: documents,
updatedAt: new Date()
});
res.json({ success: true, message: 'Documents uploaded successfully' });
} catch (error) {
console.error('Upload documents error:', error);
res.status(500).json({ success: false, message: 'Error uploading documents' });
}
};

View File

@ -0,0 +1,269 @@
const bcrypt = require('bcryptjs');
const { User, AuditLog } = require('../models');
const { generateToken } = require('../config/auth');
const { AUDIT_ACTIONS } = require('../config/constants');
// Register new user
exports.register = async (req, res) => {
try {
const { email, password, fullName, role, phone, region, zone } = req.body;
// Validate input
if (!email || !password || !fullName || !role) {
return res.status(400).json({
success: false,
message: 'Email, password, full name, and role are required'
});
}
// Check if user already exists
const existingUser = await User.findOne({ where: { email } });
if (existingUser) {
return res.status(400).json({
success: false,
message: 'User with this email already exists'
});
}
// Hash password
const hashedPassword = await bcrypt.hash(password, 10);
// Insert user
const user = await User.create({
email,
password: hashedPassword,
name: fullName,
role,
phone,
region,
zone
});
// Log audit
await AuditLog.create({
userId: user.id,
action: AUDIT_ACTIONS.CREATED,
entityType: 'user',
entityId: user.id
});
res.status(201).json({
success: true,
message: 'User registered successfully',
userId: user.id
});
} catch (error) {
console.error('Register error:', error);
res.status(500).json({
success: false,
message: 'Error registering user'
});
}
};
// Login
exports.login = async (req, res) => {
try {
const { email, password } = req.body;
// Validate input
if (!email || !password) {
return res.status(400).json({
success: false,
message: 'Email and password are required'
});
}
// Get user
const user = await User.findOne({ where: { email } });
if (!user) {
return res.status(401).json({
success: false,
message: 'Invalid email or password'
});
}
// Check if account is active
if (user.status !== 'active') {
return res.status(403).json({
success: false,
message: 'Account is deactivated'
});
}
// Verify password
const isValidPassword = await bcrypt.compare(password, user.password);
if (!isValidPassword) {
return res.status(401).json({
success: false,
message: 'Invalid email or password'
});
}
// Update last login
await user.update({ lastLogin: new Date() });
// Generate token
const token = generateToken(user);
// Log audit
await AuditLog.create({
userId: user.id,
action: 'user_login',
entityType: 'user',
entityId: user.id
});
res.json({
success: true,
token,
user: {
id: user.id,
email: user.email,
fullName: user.name,
role: user.role,
region: user.region,
zone: user.zone
}
});
} catch (error) {
console.error('Login error:', error);
res.status(500).json({
success: false,
message: 'Error during login'
});
}
};
// Get profile
exports.getProfile = async (req, res) => {
try {
const user = await User.findByPk(req.user.id, {
attributes: ['id', 'email', 'name', 'role', 'region', 'zone', 'phone', 'createdAt']
});
if (!user) {
return res.status(404).json({
success: false,
message: 'User not found'
});
}
res.json({
success: true,
user: {
id: user.id,
email: user.email,
fullName: user.name,
role: user.role,
region: user.region,
zone: user.zone,
phone: user.phone,
createdAt: user.createdAt
}
});
} catch (error) {
console.error('Get profile error:', error);
res.status(500).json({
success: false,
message: 'Error fetching profile'
});
}
};
// Update profile
exports.updateProfile = async (req, res) => {
try {
const { fullName, phone } = req.body;
const user = await User.findByPk(req.user.id);
if (!user) {
return res.status(404).json({
success: false,
message: 'User not found'
});
}
await user.update({
name: fullName || user.name,
phone: phone || user.phone
});
// Log audit
await AuditLog.create({
userId: req.user.id,
action: AUDIT_ACTIONS.UPDATED,
entityType: 'user',
entityId: req.user.id
});
res.json({
success: true,
message: 'Profile updated successfully'
});
} catch (error) {
console.error('Update profile error:', error);
res.status(500).json({
success: false,
message: 'Error updating profile'
});
}
};
// Change password
exports.changePassword = async (req, res) => {
try {
const { currentPassword, newPassword } = req.body;
if (!currentPassword || !newPassword) {
return res.status(400).json({
success: false,
message: 'Current password and new password are required'
});
}
// Get current user
const user = await User.findByPk(req.user.id);
if (!user) {
return res.status(404).json({
success: false,
message: 'User not found'
});
}
// Verify current password
const isValid = await bcrypt.compare(currentPassword, user.password);
if (!isValid) {
return res.status(401).json({
success: false,
message: 'Current password is incorrect'
});
}
// Hash new password
const hashedPassword = await bcrypt.hash(newPassword, 10);
// Update password
await user.update({ password: hashedPassword });
// Log audit
await AuditLog.create({
userId: req.user.id,
action: 'password_changed',
entityType: 'user',
entityId: req.user.id
});
res.json({
success: true,
message: 'Password changed successfully'
});
} catch (error) {
console.error('Change password error:', error);
res.status(500).json({
success: false,
message: 'Error changing password'
});
}
};

View File

@ -0,0 +1,183 @@
const { ConstitutionalChange, Outlet, User, Worknote } = require('../models');
const { v4: uuidv4 } = require('uuid');
const { Op } = require('sequelize'); // Required for Op.or
exports.submitRequest = async (req, res) => {
try {
const {
outletId, changeType, currentConstitution, proposedConstitution,
reason, effectiveDate, newEntityDetails
} = req.body;
const requestId = `CC-${Date.now()}-${uuidv4().substring(0, 4).toUpperCase()}`;
const request = await ConstitutionalChange.create({
requestId,
outletId,
dealerId: req.user.id,
changeType,
description: reason,
currentConstitution,
proposedConstitution,
effectiveDate,
newEntityDetails: JSON.stringify(newEntityDetails), // Store as JSON string
currentStage: 'RBM Review',
status: 'Pending',
progressPercentage: 0,
timeline: [{
stage: 'Submitted',
timestamp: new Date(),
user: req.user.full_name, // Assuming req.user.full_name is available
action: 'Request submitted'
}]
});
res.status(201).json({
success: true,
message: 'Constitutional change request submitted successfully',
requestId: request.requestId
});
} catch (error) {
console.error('Submit constitutional change error:', error);
res.status(500).json({ success: false, message: 'Error submitting request' });
}
};
exports.getRequests = async (req, res) => {
try {
const where = {};
if (req.user.role === 'Dealer') {
where.dealerId = req.user.id;
}
const requests = await ConstitutionalChange.findAll({
where,
include: [
{
model: Outlet,
as: 'outlet',
attributes: ['code', 'name']
},
{
model: User,
as: 'dealer',
attributes: ['full_name'] // Changed from 'name' to 'full_name' based on original code
}
],
order: [['createdAt', 'DESC']]
});
res.json({ success: true, requests });
} catch (error) {
console.error('Get constitutional changes error:', error);
res.status(500).json({ success: false, message: 'Error fetching requests' });
}
};
exports.getRequestById = async (req, res) => {
try {
const { id } = req.params;
const request = await ConstitutionalChange.findOne({
where: {
[Op.or]: [
{ id },
{ requestId: id }
]
},
include: [
{
model: Outlet,
as: 'outlet'
},
{
model: User,
as: 'dealer',
attributes: ['full_name', 'email'] // Changed from 'name' to 'full_name'
},
{
model: Worknote,
as: 'worknotes' // Assuming Worknote model is associated as 'worknotes'
}
]
});
if (!request) {
return res.status(404).json({ success: false, message: 'Request not found' });
}
res.json({ success: true, request });
} catch (error) {
console.error('Get constitutional change details error:', error);
res.status(500).json({ success: false, message: 'Error fetching details' });
}
};
exports.takeAction = async (req, res) => {
try {
const { id } = req.params;
const { action, comments } = req.body;
const request = await ConstitutionalChange.findOne({
where: {
[Op.or]: [
{ id },
{ requestId: id }
]
}
});
if (!request) {
return res.status(404).json({ success: false, message: 'Request not found' });
}
const timeline = [...request.timeline, {
stage: 'Review',
timestamp: new Date(),
user: req.user.full_name, // Assuming req.user.full_name is available
action,
remarks: comments
}];
await request.update({
status: action, // Assuming action directly maps to status (e.g., 'Approved', 'Rejected')
timeline,
updatedAt: new Date()
});
res.json({ success: true, message: `Request ${action.toLowerCase()} successfully` });
} catch (error) {
console.error('Take action error:', error);
res.status(500).json({ success: false, message: 'Error processing action' });
}
};
exports.uploadDocuments = async (req, res) => {
try {
const { id } = req.params;
const { documents } = req.body;
const request = await ConstitutionalChange.findOne({
where: {
[Op.or]: [
{ id },
{ requestId: id }
]
}
});
if (!request) {
return res.status(404).json({ success: false, message: 'Request not found' });
}
await request.update({
documents: documents, // Assuming documents is an array or object that can be stored directly
updatedAt: new Date()
});
res.json({ success: true, message: 'Documents uploaded successfully' });
} catch (error) {
console.error('Upload documents error:', error);
res.status(500).json({ success: false, message: 'Error uploading documents' });
}
};

View File

@ -0,0 +1,99 @@
const { FinancePayment, FnF, Application, Resignation, User, Outlet } = require('../models');
exports.getOnboardingPayments = async (req, res) => {
try {
const payments = await FinancePayment.findAll({
include: [{
model: Application,
as: 'application',
attributes: ['applicantName', 'applicationId']
}],
order: [['createdAt', 'ASC']]
});
res.json({ success: true, payments });
} catch (error) {
console.error('Get onboarding payments error:', error);
res.status(500).json({ success: false, message: 'Error fetching payments' });
}
};
exports.getFnFSettlements = async (req, res) => {
try {
const settlements = await FnF.findAll({
include: [
{
model: Resignation,
as: 'resignation',
attributes: ['resignationId']
},
{
model: Outlet, // Need to ensure Outlet is imported or associated
as: 'outlet',
include: [{
model: User,
as: 'dealer',
attributes: ['name']
}]
}
],
order: [['createdAt', 'DESC']]
});
res.json({ success: true, settlements });
} catch (error) {
console.error('Get F&F settlements error:', error);
res.status(500).json({ success: false, message: 'Error fetching settlements' });
}
};
exports.updatePayment = async (req, res) => {
try {
const { id } = req.params;
const { paidDate, amount, paymentMode, transactionReference, status } = req.body;
const payment = await FinancePayment.findByPk(id);
if (!payment) {
return res.status(404).json({ success: false, message: 'Payment not found' });
}
await payment.update({
paymentDate: paidDate || payment.paymentDate,
amount: amount || payment.amount,
transactionId: transactionReference || payment.transactionId,
paymentStatus: status || payment.paymentStatus,
updatedAt: new Date()
});
res.json({ success: true, message: 'Payment updated successfully' });
} catch (error) {
console.error('Update payment error:', error);
res.status(500).json({ success: false, message: 'Error updating payment' });
}
};
exports.updateFnF = async (req, res) => {
try {
const { id } = req.params;
const {
inventoryClearance, sparesClearance, accountsClearance, legalClearance,
finalSettlementAmount, status
} = req.body;
const fnf = await FnF.findByPk(id);
if (!fnf) {
return res.status(404).json({ success: false, message: 'F&F settlement not found' });
}
await fnf.update({
status: status || fnf.status,
netAmount: finalSettlementAmount || fnf.netAmount,
updatedAt: new Date()
});
res.json({ success: true, message: 'F&F settlement updated successfully' });
} catch (error) {
console.error('Update F&F error:', error);
res.status(500).json({ success: false, message: 'Error updating F&F settlement' });
}
};

View File

@ -0,0 +1,121 @@
const { Region, Zone } = require('../models');
exports.getRegions = async (req, res) => {
try {
const regions = await Region.findAll({
order: [['name', 'ASC']]
});
res.json({ success: true, regions });
} catch (error) {
console.error('Get regions error:', error);
res.status(500).json({ success: false, message: 'Error fetching regions' });
}
};
exports.createRegion = async (req, res) => {
try {
const { regionName } = req.body;
if (!regionName) {
return res.status(400).json({ success: false, message: 'Region name is required' });
}
await Region.create({ name: regionName });
res.status(201).json({ success: true, message: 'Region created successfully' });
} catch (error) {
console.error('Create region error:', error);
res.status(500).json({ success: false, message: 'Error creating region' });
}
};
exports.updateRegion = async (req, res) => {
try {
const { id } = req.params;
const { regionName, isActive } = req.body;
const region = await Region.findByPk(id);
if (!region) {
return res.status(404).json({ success: false, message: 'Region not found' });
}
await region.update({
name: regionName || region.name,
updatedAt: new Date()
});
res.json({ success: true, message: 'Region updated successfully' });
} catch (error) {
console.error('Update region error:', error);
res.status(500).json({ success: false, message: 'Error updating region' });
}
};
exports.getZones = async (req, res) => {
try {
const { regionId } = req.query;
const where = {};
if (regionId) {
where.regionId = regionId;
}
const zones = await Zone.findAll({
where,
include: [{
model: Region,
as: 'region',
attributes: ['name']
}],
order: [['name', 'ASC']]
});
res.json({ success: true, zones });
} catch (error) {
console.error('Get zones error:', error);
res.status(500).json({ success: false, message: 'Error fetching zones' });
}
};
exports.createZone = async (req, res) => {
try {
const { regionId, zoneName, zoneCode } = req.body;
if (!regionId || !zoneName) {
return res.status(400).json({ success: false, message: 'Region ID and zone name are required' });
}
await Zone.create({
regionId,
name: zoneName
});
res.status(201).json({ success: true, message: 'Zone created successfully' });
} catch (error) {
console.error('Create zone error:', error);
res.status(500).json({ success: false, message: 'Error creating zone' });
}
};
exports.updateZone = async (req, res) => {
try {
const { id } = req.params;
const { zoneName, zoneCode, isActive } = req.body;
const zone = await Zone.findByPk(id);
if (!zone) {
return res.status(404).json({ success: false, message: 'Zone not found' });
}
await zone.update({
name: zoneName || zone.name,
updatedAt: new Date()
});
res.json({ success: true, message: 'Zone updated successfully' });
} catch (error) {
console.error('Update zone error:', error);
res.status(500).json({ success: false, message: 'Error updating zone' });
}
};

View File

@ -0,0 +1,184 @@
const { Outlet, User, Resignation } = require('../models');
// Get all outlets for logged-in dealer
exports.getOutlets = async (req, res) => {
try {
const where = {};
if (req.user.role === 'Dealer') {
where.dealerId = req.user.id;
}
const outlets = await Outlet.findAll({
where,
include: [
{
model: User,
as: 'dealer',
attributes: ['name', 'email']
},
{
model: Resignation,
as: 'resignations',
required: false,
where: {
status: { [Op.notIn]: ['Completed', 'Rejected'] }
}
}
],
order: [['createdAt', 'DESC']]
});
res.json({
success: true,
outlets
});
} catch (error) {
console.error('Get outlets error:', error);
res.status(500).json({
success: false,
message: 'Error fetching outlets'
});
}
};
// Get specific outlet details
exports.getOutletById = async (req, res) => {
try {
const { id } = req.params;
const outlet = await Outlet.findByPk(id, {
include: [{
model: User,
as: 'dealer',
attributes: ['name', 'email', 'phone']
}]
});
if (!outlet) {
return res.status(404).json({
success: false,
message: 'Outlet not found'
});
}
// Check if dealer can access this outlet
if (req.user.role === 'Dealer' && outlet.dealerId !== req.user.id) {
return res.status(403).json({
success: false,
message: 'Access denied'
});
}
res.json({
success: true,
outlet
});
} catch (error) {
console.error('Get outlet error:', error);
res.status(500).json({
success: false,
message: 'Error fetching outlet'
});
}
};
// Create new outlet (admin only)
exports.createOutlet = async (req, res) => {
try {
const {
dealerId,
code,
name,
type,
address,
city,
state,
pincode,
establishedDate,
latitude,
longitude
} = req.body;
// Validate required fields
if (!dealerId || !code || !name || !type || !address || !city || !state) {
return res.status(400).json({
success: false,
message: 'Missing required fields'
});
}
// Check if code already exists
const existing = await Outlet.findOne({ where: { code } });
if (existing) {
return res.status(400).json({
success: false,
message: 'Outlet code already exists'
});
}
const outlet = await Outlet.create({
dealerId,
code,
name,
type,
address,
city,
state,
pincode,
establishedDate,
latitude,
longitude
});
res.status(201).json({
success: true,
message: 'Outlet created successfully',
outletId: outlet.id
});
} catch (error) {
console.error('Create outlet error:', error);
res.status(500).json({
success: false,
message: 'Error creating outlet'
});
}
};
// Update outlet
exports.updateOutlet = async (req, res) => {
try {
const { id } = req.params;
const { name, address, city, state, pincode, status, latitude, longitude } = req.body;
const outlet = await Outlet.findByPk(id);
if (!outlet) {
return res.status(404).json({
success: false,
message: 'Outlet not found'
});
}
await outlet.update({
name: name || outlet.name,
address: address || outlet.address,
city: city || outlet.city,
state: state || outlet.state,
pincode: pincode || outlet.pincode,
status: status || outlet.status,
latitude: latitude || outlet.latitude,
longitude: longitude || outlet.longitude,
updatedAt: new Date()
});
res.json({
success: true,
message: 'Outlet updated successfully'
});
} catch (error) {
console.error('Update outlet error:', error);
res.status(500).json({
success: false,
message: 'Error updating outlet'
});
}
};

View File

@ -0,0 +1,238 @@
const { RelocationRequest, Outlet, User, Worknote } = require('../models');
const { Op } = require('sequelize');
const { v4: uuidv4 } = require('uuid');
exports.submitRequest = async (req, res) => {
try {
const {
outletId, relocationType, currentAddress, currentCity, currentState,
currentLatitude, currentLongitude, proposedAddress, proposedCity,
proposedState, proposedLatitude, proposedLongitude, reason, proposedDate
} = req.body;
const requestId = `REL-${Date.now()}-${uuidv4().substring(0, 4).toUpperCase()}`;
const request = await RelocationRequest.create({
requestId,
outletId,
dealerId: req.user.id,
relocationType,
currentAddress,
currentCity,
currentState,
currentLatitude,
currentLongitude,
proposedAddress,
proposedCity,
proposedState,
proposedLatitude,
proposedLongitude,
reason,
proposedDate,
currentStage: 'RBM Review',
status: 'Pending',
progressPercentage: 0,
timeline: [{
stage: 'Submitted',
timestamp: new Date(),
user: req.user.full_name, // Assuming req.user.full_name is available
action: 'Request submitted'
}]
});
res.status(201).json({
success: true,
message: 'Relocation request submitted successfully',
requestId: request.requestId
});
} catch (error) {
console.error('Submit relocation error:', error);
res.status(500).json({ success: false, message: 'Error submitting request' });
}
};
exports.getRequests = async (req, res) => {
try {
const where = {};
if (req.user.role === 'Dealer') {
where.dealerId = req.user.id;
}
const requests = await RelocationRequest.findAll({
where,
include: [
{
model: Outlet,
as: 'outlet',
attributes: ['code', 'name']
},
{
model: User,
as: 'dealer',
attributes: ['full_name'] // Changed 'name' to 'full_name' based on original query
}
],
order: [['createdAt', 'DESC']]
});
res.json({ success: true, requests });
} catch (error) {
console.error('Get relocation requests error:', error);
res.status(500).json({ success: false, message: 'Error fetching requests' });
}
};
exports.getRequestById = async (req, res) => {
try {
const { id } = req.params;
const request = await RelocationRequest.findOne({
where: {
[Op.or]: [
{ id },
{ requestId: id }
]
},
include: [
{
model: Outlet,
as: 'outlet'
},
{
model: User,
as: 'dealer',
attributes: ['full_name', 'email'] // Changed 'name' to 'full_name'
},
{
model: Worknote,
as: 'worknotes', // Assuming Worknote model is for workflow/comments
include: [{
model: User,
as: 'actionedBy', // Assuming Worknote has an association to User for actioned_by
attributes: ['full_name']
}],
order: [['createdAt', 'ASC']]
}
]
});
if (!request) {
return res.status(404).json({ success: false, message: 'Request not found' });
}
// Calculate distance between current and proposed location (retained from original)
if (request.currentLatitude && request.proposedLatitude) {
const distance = calculateDistance(
request.currentLatitude, request.currentLongitude,
request.proposedLatitude, request.proposedLongitude
);
request.dataValues.distance = `${distance.toFixed(2)} km`; // Add to dataValues for response
}
res.json({ success: true, request });
} catch (error) {
console.error('Get relocation details error:', error);
res.status(500).json({ success: false, message: 'Error fetching details' });
}
};
exports.takeAction = async (req, res) => {
try {
const { id } = req.params;
const { action, comments } = req.body;
const request = await RelocationRequest.findOne({
where: {
[Op.or]: [
{ id },
{ requestId: id }
]
}
});
if (!request) {
return res.status(404).json({ success: false, message: 'Request not found' });
}
// Update status and current_stage based on action
let newStatus = request.status;
let newCurrentStage = request.currentStage;
if (action === 'Approved') {
newStatus = 'Approved';
// Assuming next stage logic would be here, e.g., 'Final Approval'
} else if (action === 'Rejected') {
newStatus = 'Rejected';
} else if (action === 'Forwarded to RBM') {
newCurrentStage = 'RBM Review';
} else if (action === 'Forwarded to ZBM') {
newCurrentStage = 'ZBM Review';
} else if (action === 'Forwarded to HO') {
newCurrentStage = 'HO Review';
}
// Create a worknote entry
await Worknote.create({
requestId: request.id,
stage: newCurrentStage, // Or the specific stage where action was taken
action: action,
comments: comments,
actionedBy: req.user.id,
actionedAt: new Date()
});
// Update the request status and current stage
await request.update({
status: newStatus,
currentStage: newCurrentStage,
updatedAt: new Date()
});
res.json({ success: true, message: `Request ${action.toLowerCase()} successfully` });
} catch (error) {
console.error('Take action error:', error);
res.status(500).json({ success: false, message: 'Error processing action' });
}
};
exports.uploadDocuments = async (req, res) => {
try {
const { id } = req.params;
const { documents } = req.body;
const request = await RelocationRequest.findOne({
where: {
[Op.or]: [
{ id },
{ requestId: id }
]
}
});
if (!request) {
return res.status(404).json({ success: false, message: 'Request not found' });
}
await request.update({
documents: documents,
updatedAt: new Date()
});
res.json({ success: true, message: 'Documents uploaded successfully' });
} catch (error) {
console.error('Upload documents error:', error);
res.status(500).json({ success: false, message: 'Error uploading documents' });
}
};
// Helper function to calculate distance between two coordinates
function calculateDistance(lat1, lon1, lat2, lon2) {
const R = 6371; // Radius of Earth in km
const dLat = (lat2 - lat1) * Math.PI / 180;
const dLon = (lon2 - lon1) * Math.PI / 180;
const a = Math.sin(dLat / 2) * Math.sin(dLat / 2) +
Math.cos(lat1 * Math.PI / 180) * Math.cos(lat2 * Math.PI / 180) *
Math.sin(dLon / 2) * Math.sin(dLon / 2);
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
return R * c;
}

View File

@ -0,0 +1,417 @@
const db = require('../models');
const logger = require('../utils/logger');
const { RESIGNATION_STAGES, AUDIT_ACTIONS, ROLES } = require('../config/constants');
const { Op } = require('sequelize');
// Generate unique resignation ID
const generateResignationId = async () => {
const count = await db.Resignation.count();
return `RES-${String(count + 1).padStart(3, '0')}`;
};
// Calculate progress percentage based on stage
const calculateProgress = (stage) => {
const stageProgress = {
[RESIGNATION_STAGES.ASM]: 15,
[RESIGNATION_STAGES.RBM]: 30,
[RESIGNATION_STAGES.ZBH]: 45,
[RESIGNATION_STAGES.NBH]: 60,
[RESIGNATION_STAGES.DD_ADMIN]: 70,
[RESIGNATION_STAGES.LEGAL]: 80,
[RESIGNATION_STAGES.FINANCE]: 90,
[RESIGNATION_STAGES.FNF_INITIATED]: 95,
[RESIGNATION_STAGES.COMPLETED]: 100,
[RESIGNATION_STAGES.REJECTED]: 0
};
return stageProgress[stage] || 0;
};
// Create resignation request (Dealer only)
exports.createResignation = async (req, res, next) => {
const transaction = await db.sequelize.transaction();
try {
const { outletId, resignationType, lastOperationalDateSales, lastOperationalDateServices, reason, additionalInfo } = req.body;
const dealerId = req.user.id;
// Verify outlet belongs to dealer
const outlet = await db.Outlet.findOne({
where: { id: outletId, dealerId }
});
if (!outlet) {
await transaction.rollback();
return res.status(404).json({
success: false,
message: 'Outlet not found or does not belong to you'
});
}
// Check if outlet already has active resignation
const existingResignation = await db.Resignation.findOne({
where: {
outletId,
status: { [Op.notIn]: ['Completed', 'Rejected'] }
}
});
if (existingResignation) {
await transaction.rollback();
return res.status(400).json({
success: false,
message: 'This outlet already has an active resignation request'
});
}
// Generate resignation ID
const resignationId = await generateResignationId();
// Create resignation
const resignation = await db.Resignation.create({
resignationId,
outletId,
dealerId,
resignationType,
lastOperationalDateSales,
lastOperationalDateServices,
reason,
additionalInfo,
currentStage: RESIGNATION_STAGES.ASM,
status: 'ASM Review',
progressPercentage: 15,
timeline: [{
stage: 'Submitted',
timestamp: new Date(),
user: req.user.name,
action: 'Resignation request submitted'
}]
}, { transaction });
// Update outlet status
await outlet.update({
status: 'Pending Resignation'
}, { transaction });
// Create audit log
await db.AuditLog.create({
userId: req.user.id,
userName: req.user.name,
userRole: req.user.role,
action: AUDIT_ACTIONS.CREATED,
entityType: 'resignation',
entityId: resignation.id,
changes: { created: resignation.toJSON() }
}, { transaction });
await transaction.commit();
logger.info(`Resignation request created: ${resignationId} by dealer: ${req.user.email}`);
// TODO: Send email notification to ASM/DD Admin
res.status(201).json({
success: true,
message: 'Resignation request submitted successfully',
resignationId: resignation.resignationId,
resignation: resignation.toJSON()
});
} catch (error) {
await transaction.rollback();
logger.error('Error creating resignation:', error);
next(error);
}
};
// Get resignations list (role-based filtering)
exports.getResignations = async (req, res, next) => {
try {
const { status, region, zone, page = 1, limit = 10 } = req.query;
const offset = (page - 1) * limit;
// Build where clause based on user role
let where = {};
if (req.user.role === ROLES.DEALER) {
// Dealers see only their resignations
where.dealerId = req.user.id;
} else if (req.user.region && ![ROLES.NBH, ROLES.DD_HEAD, ROLES.DD_LEAD, ROLES.SUPER_ADMIN].includes(req.user.role)) {
// Regional users see resignations in their region
where['$outlet.region$'] = req.user.region;
}
if (status) {
where.status = status;
}
// Get resignations
const { count, rows: resignations } = await db.Resignation.findAndCountAll({
where,
include: [
{
model: db.Outlet,
as: 'outlet',
attributes: ['id', 'code', 'name', 'type', 'city', 'state', 'region', 'zone']
},
{
model: db.User,
as: 'dealer',
attributes: ['id', 'name', 'email', 'phone']
}
],
order: [['submittedOn', 'DESC']],
limit: parseInt(limit),
offset: parseInt(offset)
});
res.json({
success: true,
resignations,
pagination: {
total: count,
page: parseInt(page),
pages: Math.ceil(count / limit),
limit: parseInt(limit)
}
});
} catch (error) {
logger.error('Error fetching resignations:', error);
next(error);
}
};
// Get resignation details
exports.getResignationById = async (req, res, next) => {
try {
const { id } = req.params;
const resignation = await db.Resignation.findOne({
where: { id },
include: [
{
model: db.Outlet,
as: 'outlet',
include: [
{
model: db.User,
as: 'dealer',
attributes: ['id', 'name', 'email', 'phone']
}
]
},
{
model: db.Worknote,
as: 'worknotes',
order: [['timestamp', 'DESC']]
}
]
});
if (!resignation) {
return res.status(404).json({
success: false,
message: 'Resignation not found'
});
}
// Check access permissions
if (req.user.role === ROLES.DEALER && resignation.dealerId !== req.user.id) {
return res.status(403).json({
success: false,
message: 'Access denied'
});
}
res.json({
success: true,
resignation
});
} catch (error) {
logger.error('Error fetching resignation details:', error);
next(error);
}
};
// Approve resignation (move to next stage)
exports.approveResignation = async (req, res, next) => {
const transaction = await db.sequelize.transaction();
try {
const { id } = req.params;
const { remarks } = req.body;
const resignation = await db.Resignation.findByPk(id, {
include: [{ model: db.Outlet, as: 'outlet' }]
});
if (!resignation) {
await transaction.rollback();
return res.status(404).json({
success: false,
message: 'Resignation not found'
});
}
// Determine next stage based on current stage
const stageFlow = {
[RESIGNATION_STAGES.ASM]: RESIGNATION_STAGES.RBM,
[RESIGNATION_STAGES.RBM]: RESIGNATION_STAGES.ZBH,
[RESIGNATION_STAGES.ZBH]: RESIGNATION_STAGES.NBH,
[RESIGNATION_STAGES.NBH]: RESIGNATION_STAGES.DD_ADMIN,
[RESIGNATION_STAGES.DD_ADMIN]: RESIGNATION_STAGES.LEGAL,
[RESIGNATION_STAGES.LEGAL]: RESIGNATION_STAGES.FINANCE,
[RESIGNATION_STAGES.FINANCE]: RESIGNATION_STAGES.FNF_INITIATED,
[RESIGNATION_STAGES.FNF_INITIATED]: RESIGNATION_STAGES.COMPLETED
};
const nextStage = stageFlow[resignation.currentStage];
if (!nextStage) {
await transaction.rollback();
return res.status(400).json({
success: false,
message: 'Cannot approve from current stage'
});
}
// Update resignation
const timeline = [...resignation.timeline, {
stage: nextStage,
timestamp: new Date(),
user: req.user.name,
action: 'Approved',
remarks
}];
await resignation.update({
currentStage: nextStage,
status: nextStage === RESIGNATION_STAGES.COMPLETED ? 'Completed' : `${nextStage} Review`,
progressPercentage: calculateProgress(nextStage),
timeline
}, { transaction });
// If completed, update outlet status
if (nextStage === RESIGNATION_STAGES.COMPLETED) {
await resignation.outlet.update({
status: 'Closed'
}, { transaction });
}
// Create audit log
await db.AuditLog.create({
userId: req.user.id,
userName: req.user.name,
userRole: req.user.role,
action: AUDIT_ACTIONS.APPROVED,
entityType: 'resignation',
entityId: resignation.id,
changes: {
from: resignation.currentStage,
to: nextStage,
remarks
}
}, { transaction });
await transaction.commit();
logger.info(`Resignation ${resignation.resignationId} approved to ${nextStage} by ${req.user.email}`);
// TODO: Send email notification to next approver
res.json({
success: true,
message: 'Resignation approved successfully',
nextStage,
resignation
});
} catch (error) {
await transaction.rollback();
logger.error('Error approving resignation:', error);
next(error);
}
};
// Reject resignation
exports.rejectResignation = async (req, res, next) => {
const transaction = await db.sequelize.transaction();
try {
const { id } = req.params;
const { reason } = req.body;
if (!reason) {
await transaction.rollback();
return res.status(400).json({
success: false,
message: 'Rejection reason is required'
});
}
const resignation = await db.Resignation.findByPk(id, {
include: [{ model: db.Outlet, as: 'outlet' }]
});
if (!resignation) {
await transaction.rollback();
return res.status(404).json({
success: false,
message: 'Resignation not found'
});
}
// Update resignation
const timeline = [...resignation.timeline, {
stage: 'Rejected',
timestamp: new Date(),
user: req.user.name,
action: 'Rejected',
reason
}];
await resignation.update({
currentStage: RESIGNATION_STAGES.REJECTED,
status: 'Rejected',
progressPercentage: 0,
rejectionReason: reason,
timeline
}, { transaction });
// Update outlet status back to Active
await resignation.outlet.update({
status: 'Active'
}, { transaction });
// Create audit log
await db.AuditLog.create({
userId: req.user.id,
userName: req.user.name,
userRole: req.user.role,
action: AUDIT_ACTIONS.REJECTED,
entityType: 'resignation',
entityId: resignation.id,
changes: { reason }
}, { transaction });
await transaction.commit();
logger.info(`Resignation ${resignation.resignationId} rejected by ${req.user.email}`);
// TODO: Send email notification to dealer
res.json({
success: true,
message: 'Resignation rejected',
resignation
});
} catch (error) {
await transaction.rollback();
logger.error('Error rejecting resignation:', error);
next(error);
}
};
module.exports = exports;

View File

@ -0,0 +1,84 @@
const { Worknote, User } = require('../models');
exports.addWorknote = async (req, res) => {
try {
const { requestId, requestType, message, isInternal } = req.body;
if (!requestId || !requestType || !message) {
return res.status(400).json({
success: false,
message: 'Request ID, type, and message are required'
});
}
await Worknote.create({
requestId,
requestType,
userId: req.user.id,
content: message,
isInternal: isInternal || false
});
res.status(201).json({
success: true,
message: 'Worknote added successfully'
});
} catch (error) {
console.error('Add worknote error:', error);
res.status(500).json({ success: false, message: 'Error adding worknote' });
}
};
exports.getWorknotes = async (req, res) => {
try {
const { requestId } = req.params;
const { requestType } = req.query;
const worknotes = await Worknote.findAll({
where: {
requestId,
requestType
},
include: [{
model: User,
as: 'author',
attributes: ['name', 'role']
}],
order: [['createdAt', 'DESC']]
});
res.json({
success: true,
worknotes
});
} catch (error) {
console.error('Get worknotes error:', error);
res.status(500).json({ success: false, message: 'Error fetching worknotes' });
}
};
exports.deleteWorknote = async (req, res) => {
try {
const { id } = req.params;
const worknote = await Worknote.findByPk(id);
if (!worknote) {
return res.status(404).json({ success: false, message: 'Worknote not found' });
}
// Only allow user who created it or admin to delete
if (worknote.userId !== req.user.id && req.user.role !== 'Super Admin') {
return res.status(403).json({ success: false, message: 'Access denied' });
}
await worknote.destroy();
res.json({
success: true,
message: 'Worknote deleted successfully'
});
} catch (error) {
console.error('Delete worknote error:', error);
res.status(500).json({ success: false, message: 'Error deleting worknote' });
}
};

View File

@ -0,0 +1,65 @@
# Backend Alignment Analysis Report (v1.4 Compliance)
Based on the review of `Comparison_Summary_v1.0_vs_v1.4.md`, `Re_New_Dealer_Onboard_TWO.md`, and `dealer_onboard_backend_schema.mermaid`, here is the analysis of the current backend implementation status and alignment gaps.
## 1. Executive Summary
The current backend implementation is in an **early stage (Foundation)** and lacks the majority of the complex workflows and governance features required by Version 1.4 of the documentation. While basic models for Applications, Resignations, and Self-Service (Constitutional/Relocation) exist, the business logic, approval hierarchies, and supporting modules are missing.
---
## 2. Critical Gaps & Missing Modules
### 2.1 Missing Business Modules
* **Termination Module (CRITICAL)**: Completely missing from the codebase. There are no models, routes, or controllers for handling dealer termination.
* **Dealer Code Manual Trigger**: Current documentation (1.1.3) requires **DD Admin** to manually trigger code generation in SAP. The code lacks this control point.
* **LOI & LOA Sub-Workflows**: Documentation (6.16, 6.18) describes complex approval and document issuance processes for Letters of Intent and Letters of Appointment. These are currently simplified or non-existent in the code.
* **Questionnaire & Scoring (KT Matrix)**: Module for automated questionnaire scoring, rankings, and KT Matrix interview evaluation is missing.
* **EOR Checklist**: Detailed Essential Operating Requirements (EOR) checklist with functional team verifications is not implemented.
* **Inauguration Tracking**: Final stage of onboarding is missing.
### 2.2 Governance & Roles (RBAC)
* **CEO/CCO Roles**: Missing from `config/constants.js` and `User.js`. Version 1.4 requires CEO approval for Termination.
* **Super Admin Segregation**: The planned split of Super Admin into two specialized **DD Admin** roles is not reflected in the current role structure.
* **Send Back / Revoke Authority**: The `resignationController.js` only implements basic `approve/reject`. It lacks the "Send Back" logic requested for ZBH, DD Lead, DD Head, and NBH.
### 2.3 Self-Service Logic
* **Resignation Withdrawal**: Documentation allows withdrawal "only until NBH review". This restriction is not enforced in the current controller.
* **LWD-Based F&F Trigger**: F&F settlement must be triggered "strictly on the Last Working Day (LWD)". The current `FnF.js` and `resignationController.js` do not enforce this temporal bridge.
* **WhatsApp Integration**: Requirement 1.1.1 (Multi-channel alerts) is missing implementation in the notifications layer.
---
## 3. Schema Alignment Check
The `dealer_onboard_backend_schema.mermaid` provides a high-fidelity design. The physical database (`models/`) is missing approximately **70% of the tables** defined in the schema.
### Missing Tables in Code:
| Document Section | Missing Tables (Models) |
| :--- | :--- |
| **Questionnaire** | `QUESTIONNAIRES`, `SECTIONS`, `QUESTIONS`, `RESPONSES`, `SCORES` |
| **Interviews** | `INTERVIEWS`, `PARTICIPANTS`, `EVALUATIONS`, `KT_MATRIX_SCORES`, `FEEDBACK` |
| **LOI Process** | `LOI_REQUESTS`, `LOI_APPROVALS`, `LOI_DOCUMENTS_GENERATED`, `ACKNOWLEDGEMENTS` |
| **EOR / Construction** | `ARCHITECTURAL_ASSIGNMENTS`, `EOR_CHECKLISTS`, `CHECKLIST_ITEMS`, `CONSTRUCTION_PROGRESS` |
| **Termination** | `TERMINATION_REQUESTS`, `TERMINATION_APPROVALS`, `SCN_ISSUANCE` |
| **Other** | `AI_SUMMARIES`, `INAUGURATIONS`, `SECURITY_DEPOSITS` |
---
## 4. Required Backend Changes to Align with Documentation
### Phase 1: Governance & Framework (Immediate)
1. **Update `constants.js`**: Add `CEO`, `CCO` roles and `TERMINATION_STAGES`.
2. **Enhance Workflow Engine**: Implement a generic "Send Back" mechanism that tracks the previous stage and logs mandatory audit remarks.
3. **Audit Trail Expansion**: Ensure every state change captures the "Section 4.4" requirements (Uploader, Timestamp, Versioning).
### Phase 2: Workflow Refinement
1. **Sequence Correction (LOA before EOR)**: Restructure the `Application` state machine to ensure LOA issuance is a prerequisite for EOR checklist activation.
2. **LWD Enforcement**: Modify `FnF` initiation logic to check against `outlet.last_working_day`.
3. **Manual Code Trigger**: Add dedicated endpoint `/api/applications/:id/generate-code` restricted to `DD_ADMIN`.
### Phase 3: Module Completion
1. **Develop Termination Controller**: Implement the 11-step process described in Section 4.3.
2. **Questionnaire Engine**: Move from hardcoded fields to a dynamic questionnaire system as per the schema.
3. **Document Repository**: Implement the "Central Document Repository" with versioning for Statutory and Architectural documents.
---
**Status Recommendation**: The backend requires significant structural updates to meet the "Version 1.4" standards described in the documentation. High priority should be given to Role updates and the Termination module.

View File

@ -0,0 +1,56 @@
# Comparison Summary: Re_New_Dealer_Onboard.md vs Re_New_Dealer_Onboard_TWO.md
## Overview
The document `Re_New_Dealer_Onboard_TWO.md` (Version 1.4) introduces significant enhancements focused on **Dealer Self-Service**, **Executive Governance**, and **Workflow Refinements** compared to the original `Re_New_Dealer_Onboard.md` (Version 1.0).
---
## 1. Major New Features
### 1.1 Dealer Self-Service Portal (Section 12)
A major addition allowing onboarded dealers to manage their own lifecycle via the portal:
- **Resignation Initiation:** Dealers can now formally submit resignations, provide reasons, and track progress through the system.
- **Constitutional Change Management:** Supports requests to change business structures (e.g., Proprietorship to LLP/Pvt Ltd).
- **Relocation Requests:** Dealers can initiate moves to new locations, featuring map-based selection and automated distance calculations.
---
## 2. Governance & Role Enhancements
### 2.1 Executive Oversight (CEO/CCO)
- **Final Termination Authority:** Dealer terminations now require **CEO/CCO authorization**, moving decision-making to the highest level.
- **Super Admin Role:** Introduced a "Master Role" with unrestricted access to override workflows and system configurations.
### 2.2 Role-Based Access Control (RBAC)
- **Granular Visibility:** Defined "View-Only" and "Approval Visibility" for specific roles (e.g., ASM, ZM) to ensure data security and process integrity.
---
## 3. Workflow & Technical Logic Refinements
### 3.1 Dealer Code & SAP Integration
- **Manual Trigger:** Dealer codes are no longer auto-generated; they require a manual trigger by **DD Admin** for creation in SAP Master.
### 3.2 Sequence Corrections
- **LOA before EOR:** The workflow now ensures the **Letter of Authorization (LOA)** is issued *before* starting the **Essential Operating Requirements (EOR)** checklist.
### 3.3 Settlement Logic
- **LWD-Based Trigger:** F&F (Full & Final) settlements are strictly triggered on the **Last Working Day (LWD)**, ensuring accuracy regardless of when the resignation was approved.
---
## 4. Communication & Notifications
### 4.1 Multi-Channel Alerts
- **WhatsApp Support:** Added as a channel for status updates and reminders (e.g., questionnaire completion), while sensitive documents remain email-only.
- **Work Notes Governance:** Expanded "Send Back" and "Revoke" authority for senior roles, with mandatory audit remarks captured within the system.
---
## 5. Metadata & Documentation Changes
- **Change Logs:** Versions 2.0 and "Dealer Self-Service Enablement" logs were added.
- **Persona Additions:** RBM and NBH are explicitly mapped into more workflow tables.
- **Terminology:** KT Matrix is formally clarified as the **Kepner Tregoe Matrix**.
---
*Created on: 2026-01-20*

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

100
middleware/auth.js Normal file
View File

@ -0,0 +1,100 @@
const jwt = require('jsonwebtoken');
const db = require('../models');
const logger = require('../utils/logger');
const authenticate = async (req, res, next) => {
try {
// Get token from header
const authHeader = req.header('Authorization');
if (!authHeader || !authHeader.startsWith('Bearer ')) {
return res.status(401).json({
success: false,
message: 'Access denied. No token provided.'
});
}
const token = authHeader.replace('Bearer ', '');
// Verify token
const decoded = jwt.verify(token, process.env.JWT_SECRET);
// Find user
const user = await db.User.findByPk(decoded.id, {
attributes: { exclude: ['password'] }
});
if (!user) {
return res.status(401).json({
success: false,
message: 'Invalid token. User not found.'
});
}
if (user.status !== 'active') {
return res.status(401).json({
success: false,
message: 'User account is inactive.'
});
}
// Attach user to request
req.user = user;
req.token = token;
next();
} catch (error) {
logger.error('Authentication error:', error);
if (error.name === 'TokenExpiredError') {
return res.status(401).json({
success: false,
message: 'Token expired'
});
}
if (error.name === 'JsonWebTokenError') {
return res.status(401).json({
success: false,
message: 'Invalid token'
});
}
res.status(500).json({
success: false,
message: 'Authentication failed'
});
}
};
const optionalAuth = async (req, res, next) => {
try {
const authHeader = req.header('Authorization');
if (!authHeader || !authHeader.startsWith('Bearer ')) {
return next();
}
const token = authHeader.replace('Bearer ', '');
const decoded = jwt.verify(token, process.env.JWT_SECRET);
const user = await db.User.findByPk(decoded.id, {
attributes: { exclude: ['password'] }
});
if (user && user.status === 'active') {
req.user = user;
req.token = token;
}
next();
} catch (error) {
// If token is invalid/expired, just proceed without user
next();
}
};
module.exports = {
authenticate,
optionalAuth
};

View File

@ -0,0 +1,76 @@
const logger = require('../utils/logger');
const errorHandler = (err, req, res, next) => {
// Log error
logger.error('Error occurred:', {
message: err.message,
stack: err.stack,
path: req.path,
method: req.method,
ip: req.ip
});
// Sequelize validation errors
if (err.name === 'SequelizeValidationError') {
const errors = err.errors.map(e => ({
field: e.path,
message: e.message
}));
return res.status(400).json({
success: false,
message: 'Validation error',
errors
});
}
// Sequelize unique constraint errors
if (err.name === 'SequelizeUniqueConstraintError') {
return res.status(409).json({
success: false,
message: 'Resource already exists',
field: err.errors[0]?.path
});
}
// JWT errors
if (err.name === 'JsonWebTokenError') {
return res.status(401).json({
success: false,
message: 'Invalid token'
});
}
if (err.name === 'TokenExpiredError') {
return res.status(401).json({
success: false,
message: 'Token expired'
});
}
// Multer file upload errors
if (err.name === 'MulterError') {
if (err.code === 'LIMIT_FILE_SIZE') {
return res.status(413).json({
success: false,
message: 'File too large'
});
}
return res.status(400).json({
success: false,
message: err.message
});
}
// Default error
const statusCode = err.statusCode || 500;
const message = err.message || 'Internal server error';
res.status(statusCode).json({
success: false,
message,
...(process.env.NODE_ENV === 'development' && { stack: err.stack })
});
};
module.exports = errorHandler;

47
middleware/roleCheck.js Normal file
View File

@ -0,0 +1,47 @@
const { ROLES } = require('../config/constants');
const logger = require('../utils/logger');
/**
* Role-based access control middleware
* @param {Array<string>} allowedRoles - Array of roles that can access the route
* @returns {Function} Express middleware function
*/
const checkRole = (allowedRoles) => {
return (req, res, next) => {
try {
// Check if user is authenticated
if (!req.user) {
return res.status(401).json({
success: false,
message: 'Authentication required'
});
}
// Check if user role is in allowed roles
if (!allowedRoles.includes(req.user.role)) {
logger.warn(`Access denied for user ${req.user.email} (${req.user.role}) to route ${req.path}`);
return res.status(403).json({
success: false,
message: 'Access denied. Insufficient permissions.',
requiredRoles: allowedRoles,
yourRole: req.user.role
});
}
// User has required role, proceed
next();
} catch (error) {
logger.error('Role check error:', error);
res.status(500).json({
success: false,
message: 'Authorization check failed'
});
}
};
};
module.exports = {
checkRole,
ROLES
};

101
middleware/upload.js Normal file
View File

@ -0,0 +1,101 @@
const multer = require('multer');
const path = require('path');
const fs = require('fs');
const { v4: uuidv4 } = require('uuid');
// Create uploads directory if it doesn't exist
const uploadDir = process.env.UPLOAD_DIR || './uploads';
const documentsDir = path.join(uploadDir, 'documents');
const profilesDir = path.join(uploadDir, 'profiles');
const tempDir = path.join(uploadDir, 'temp');
[uploadDir, documentsDir, profilesDir, tempDir].forEach(dir => {
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true });
}
});
// Storage configuration
const storage = multer.diskStorage({
destination: (req, file, cb) => {
let folder = documentsDir;
if (req.body.uploadType === 'profile') {
folder = profilesDir;
} else if (req.body.uploadType === 'temp') {
folder = tempDir;
}
cb(null, folder);
},
filename: (req, file, cb) => {
const uniqueId = uuidv4();
const ext = path.extname(file.originalname);
const filename = `${uniqueId}${ext}`;
cb(null, filename);
}
});
// File filter
const fileFilter = (req, file, cb) => {
// Allowed file types
const allowedTypes = [
'application/pdf',
'image/jpeg',
'image/jpg',
'image/png',
'application/msword',
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
'application/vnd.ms-excel',
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
];
if (allowedTypes.includes(file.mimetype)) {
cb(null, true);
} else {
cb(new Error('Invalid file type. Only PDF, JPG, PNG, DOC, DOCX, XLS, XLSX allowed'), false);
}
};
// Multer upload configuration
const upload = multer({
storage: storage,
limits: {
fileSize: parseInt(process.env.MAX_FILE_SIZE) || 10 * 1024 * 1024 // 10MB default
},
fileFilter: fileFilter
});
// Single file upload
const uploadSingle = upload.single('file');
// Multiple files upload
const uploadMultiple = upload.array('files', 10); // Max 10 files
// Error handler for multer
const handleUploadError = (err, req, res, next) => {
if (err instanceof multer.MulterError) {
if (err.code === 'LIMIT_FILE_SIZE') {
return res.status(400).json({
success: false,
message: 'File too large. Maximum size is 10MB'
});
}
return res.status(400).json({
success: false,
message: `Upload error: ${err.message}`
});
} else if (err) {
return res.status(400).json({
success: false,
message: err.message
});
}
next();
};
module.exports = {
uploadSingle,
uploadMultiple,
handleUploadError
};

104
models/Application.js Normal file
View File

@ -0,0 +1,104 @@
const { APPLICATION_STAGES, APPLICATION_STATUS, BUSINESS_TYPES } = require('../config/constants');
module.exports = (sequelize, DataTypes) => {
const Application = sequelize.define('Application', {
id: {
type: DataTypes.UUID,
defaultValue: DataTypes.UUIDV4,
primaryKey: true
},
applicationId: {
type: DataTypes.STRING,
unique: true,
allowNull: false
},
applicantName: {
type: DataTypes.STRING,
allowNull: false
},
email: {
type: DataTypes.STRING,
allowNull: false,
validate: { isEmail: true }
},
phone: {
type: DataTypes.STRING,
allowNull: false
},
businessType: {
type: DataTypes.ENUM(Object.values(BUSINESS_TYPES)),
allowNull: false
},
preferredLocation: {
type: DataTypes.STRING,
allowNull: false
},
city: {
type: DataTypes.STRING,
allowNull: false
},
state: {
type: DataTypes.STRING,
allowNull: false
},
experienceYears: {
type: DataTypes.INTEGER,
allowNull: false
},
investmentCapacity: {
type: DataTypes.STRING,
allowNull: false
},
currentStage: {
type: DataTypes.ENUM(Object.values(APPLICATION_STAGES)),
defaultValue: APPLICATION_STAGES.DD
},
overallStatus: {
type: DataTypes.ENUM(Object.values(APPLICATION_STATUS)),
defaultValue: APPLICATION_STATUS.PENDING
},
progressPercentage: {
type: DataTypes.INTEGER,
defaultValue: 0
},
submittedBy: {
type: DataTypes.UUID,
allowNull: true,
references: {
model: 'users',
key: 'id'
}
},
documents: {
type: DataTypes.JSON,
defaultValue: []
},
timeline: {
type: DataTypes.JSON,
defaultValue: []
}
}, {
tableName: 'applications',
timestamps: true,
indexes: [
{ fields: ['applicationId'] },
{ fields: ['email'] },
{ fields: ['currentStage'] },
{ fields: ['overallStatus'] }
]
});
Application.associate = (models) => {
Application.belongsTo(models.User, {
foreignKey: 'submittedBy',
as: 'submitter'
});
Application.hasMany(models.Document, {
foreignKey: 'requestId',
as: 'uploadedDocuments',
scope: { requestType: 'application' }
});
};
return Application;
};

65
models/AuditLog.js Normal file
View File

@ -0,0 +1,65 @@
const { AUDIT_ACTIONS } = require('../config/constants');
module.exports = (sequelize, DataTypes) => {
const AuditLog = sequelize.define('AuditLog', {
id: {
type: DataTypes.UUID,
defaultValue: DataTypes.UUIDV4,
primaryKey: true
},
userId: {
type: DataTypes.UUID,
allowNull: true,
references: {
model: 'users',
key: 'id'
}
},
action: {
type: DataTypes.ENUM(Object.values(AUDIT_ACTIONS)),
allowNull: false
},
entityType: {
type: DataTypes.STRING,
allowNull: false
},
entityId: {
type: DataTypes.UUID,
allowNull: false
},
oldData: {
type: DataTypes.JSON,
allowNull: true
},
newData: {
type: DataTypes.JSON,
allowNull: true
},
ipAddress: {
type: DataTypes.STRING,
allowNull: true
},
userAgent: {
type: DataTypes.STRING,
allowNull: true
}
}, {
tableName: 'audit_logs',
timestamps: true,
indexes: [
{ fields: ['userId'] },
{ fields: ['action'] },
{ fields: ['entityType'] },
{ fields: ['entityId'] }
]
});
AuditLog.associate = (models) => {
AuditLog.belongsTo(models.User, {
foreignKey: 'userId',
as: 'user'
});
};
return AuditLog;
};

View File

@ -0,0 +1,87 @@
const { CONSTITUTIONAL_CHANGE_TYPES, CONSTITUTIONAL_STAGES } = require('../config/constants');
module.exports = (sequelize, DataTypes) => {
const ConstitutionalChange = sequelize.define('ConstitutionalChange', {
id: {
type: DataTypes.UUID,
defaultValue: DataTypes.UUIDV4,
primaryKey: true
},
requestId: {
type: DataTypes.STRING,
unique: true,
allowNull: false
},
outletId: {
type: DataTypes.UUID,
allowNull: false,
references: {
model: 'outlets',
key: 'id'
}
},
dealerId: {
type: DataTypes.UUID,
allowNull: false,
references: {
model: 'users',
key: 'id'
}
},
changeType: {
type: DataTypes.ENUM(Object.values(CONSTITUTIONAL_CHANGE_TYPES)),
allowNull: false
},
description: {
type: DataTypes.TEXT,
allowNull: false
},
currentStage: {
type: DataTypes.ENUM(Object.values(CONSTITUTIONAL_STAGES)),
defaultValue: CONSTITUTIONAL_STAGES.DD_ADMIN_REVIEW
},
status: {
type: DataTypes.STRING,
defaultValue: 'Pending'
},
progressPercentage: {
type: DataTypes.INTEGER,
defaultValue: 0
},
documents: {
type: DataTypes.JSON,
defaultValue: []
},
timeline: {
type: DataTypes.JSON,
defaultValue: []
}
}, {
tableName: 'constitutional_changes',
timestamps: true,
indexes: [
{ fields: ['requestId'] },
{ fields: ['outletId'] },
{ fields: ['dealerId'] },
{ fields: ['currentStage'] }
]
});
ConstitutionalChange.associate = (models) => {
ConstitutionalChange.belongsTo(models.Outlet, {
foreignKey: 'outletId',
as: 'outlet'
});
ConstitutionalChange.belongsTo(models.User, {
foreignKey: 'dealerId',
as: 'dealer'
});
ConstitutionalChange.hasMany(models.Worknote, {
foreignKey: 'requestId',
as: 'worknotes',
scope: { requestType: 'constitutional' }
});
};
return ConstitutionalChange;
};

64
models/Document.js Normal file
View File

@ -0,0 +1,64 @@
const { REQUEST_TYPES, DOCUMENT_TYPES } = require('../config/constants');
module.exports = (sequelize, DataTypes) => {
const Document = sequelize.define('Document', {
id: {
type: DataTypes.UUID,
defaultValue: DataTypes.UUIDV4,
primaryKey: true
},
requestId: {
type: DataTypes.UUID,
allowNull: false
},
requestType: {
type: DataTypes.ENUM(Object.values(REQUEST_TYPES)),
allowNull: false
},
documentType: {
type: DataTypes.ENUM(Object.values(DOCUMENT_TYPES)),
allowNull: false
},
fileName: {
type: DataTypes.STRING,
allowNull: false
},
fileUrl: {
type: DataTypes.STRING,
allowNull: false
},
fileSize: {
type: DataTypes.INTEGER,
allowNull: true
},
uploadedBy: {
type: DataTypes.UUID,
allowNull: false,
references: {
model: 'users',
key: 'id'
}
},
status: {
type: DataTypes.STRING,
defaultValue: 'Active'
}
}, {
tableName: 'documents',
timestamps: true,
indexes: [
{ fields: ['requestId'] },
{ fields: ['requestType'] },
{ fields: ['documentType'] }
]
});
Document.associate = (models) => {
Document.belongsTo(models.User, {
foreignKey: 'uploadedBy',
as: 'uploader'
});
};
return Document;
};

75
models/FinancePayment.js Normal file
View File

@ -0,0 +1,75 @@
const { PAYMENT_TYPES, PAYMENT_STATUS } = require('../config/constants');
module.exports = (sequelize, DataTypes) => {
const FinancePayment = sequelize.define('FinancePayment', {
id: {
type: DataTypes.UUID,
defaultValue: DataTypes.UUIDV4,
primaryKey: true
},
applicationId: {
type: DataTypes.UUID,
allowNull: false,
references: {
model: 'applications',
key: 'id'
}
},
paymentType: {
type: DataTypes.ENUM(Object.values(PAYMENT_TYPES)),
allowNull: false
},
amount: {
type: DataTypes.DECIMAL(15, 2),
allowNull: false
},
paymentStatus: {
type: DataTypes.ENUM(Object.values(PAYMENT_STATUS)),
defaultValue: PAYMENT_STATUS.PENDING
},
transactionId: {
type: DataTypes.STRING,
allowNull: true
},
paymentDate: {
type: DataTypes.DATE,
allowNull: true
},
verifiedBy: {
type: DataTypes.UUID,
allowNull: true,
references: {
model: 'users',
key: 'id'
}
},
verificationDate: {
type: DataTypes.DATE,
allowNull: true
},
remarks: {
type: DataTypes.TEXT,
allowNull: true
}
}, {
tableName: 'finance_payments',
timestamps: true,
indexes: [
{ fields: ['applicationId'] },
{ fields: ['paymentStatus'] }
]
});
FinancePayment.associate = (models) => {
FinancePayment.belongsTo(models.Application, {
foreignKey: 'applicationId',
as: 'application'
});
FinancePayment.belongsTo(models.User, {
foreignKey: 'verifiedBy',
as: 'verifier'
});
};
return FinancePayment;
};

72
models/FnF.js Normal file
View File

@ -0,0 +1,72 @@
const { FNF_STATUS } = require('../config/constants');
module.exports = (sequelize, DataTypes) => {
const FnF = sequelize.define('FnF', {
id: {
type: DataTypes.UUID,
defaultValue: DataTypes.UUIDV4,
primaryKey: true
},
resignationId: {
type: DataTypes.UUID,
allowNull: true,
references: {
model: 'resignations',
key: 'id'
}
},
outletId: {
type: DataTypes.UUID,
allowNull: false,
references: {
model: 'outlets',
key: 'id'
}
},
status: {
type: DataTypes.ENUM(Object.values(FNF_STATUS)),
defaultValue: FNF_STATUS.INITIATED
},
totalReceivables: {
type: DataTypes.DECIMAL(15, 2),
defaultValue: 0
},
totalPayables: {
type: DataTypes.DECIMAL(15, 2),
defaultValue: 0
},
netAmount: {
type: DataTypes.DECIMAL(15, 2),
defaultValue: 0
},
settlementDate: {
type: DataTypes.DATE,
allowNull: true
},
clearanceDocuments: {
type: DataTypes.JSON,
defaultValue: []
}
}, {
tableName: 'fnf_settlements',
timestamps: true,
indexes: [
{ fields: ['resignationId'] },
{ fields: ['outletId'] },
{ fields: ['status'] }
]
});
FnF.associate = (models) => {
FnF.belongsTo(models.Resignation, {
foreignKey: 'resignationId',
as: 'resignation'
});
FnF.belongsTo(models.Outlet, {
foreignKey: 'outletId',
as: 'outlet'
});
};
return FnF;
};

104
models/Outlet.js Normal file
View File

@ -0,0 +1,104 @@
const { OUTLET_TYPES, OUTLET_STATUS, REGIONS } = require('../config/constants');
module.exports = (sequelize, DataTypes) => {
const Outlet = sequelize.define('Outlet', {
id: {
type: DataTypes.UUID,
defaultValue: DataTypes.UUIDV4,
primaryKey: true
},
code: {
type: DataTypes.STRING,
unique: true,
allowNull: false
},
name: {
type: DataTypes.STRING,
allowNull: false
},
type: {
type: DataTypes.ENUM(Object.values(OUTLET_TYPES)),
allowNull: false
},
address: {
type: DataTypes.TEXT,
allowNull: false
},
city: {
type: DataTypes.STRING,
allowNull: false
},
state: {
type: DataTypes.STRING,
allowNull: false
},
pincode: {
type: DataTypes.STRING,
allowNull: false
},
latitude: {
type: DataTypes.DECIMAL(10, 8),
allowNull: true
},
longitude: {
type: DataTypes.DECIMAL(11, 8),
allowNull: true
},
status: {
type: DataTypes.ENUM(Object.values(OUTLET_STATUS)),
defaultValue: OUTLET_STATUS.ACTIVE
},
establishedDate: {
type: DataTypes.DATEONLY,
allowNull: false
},
dealerId: {
type: DataTypes.UUID,
allowNull: false,
references: {
model: 'users',
key: 'id'
}
},
region: {
type: DataTypes.ENUM(Object.values(REGIONS)),
allowNull: false
},
zone: {
type: DataTypes.STRING,
allowNull: false
}
}, {
tableName: 'outlets',
timestamps: true,
indexes: [
{ fields: ['code'] },
{ fields: ['dealerId'] },
{ fields: ['type'] },
{ fields: ['status'] },
{ fields: ['region'] },
{ fields: ['zone'] }
]
});
Outlet.associate = (models) => {
Outlet.belongsTo(models.User, {
foreignKey: 'dealerId',
as: 'dealer'
});
Outlet.hasMany(models.Resignation, {
foreignKey: 'outletId',
as: 'resignations'
});
Outlet.hasMany(models.ConstitutionalChange, {
foreignKey: 'outletId',
as: 'constitutionalChanges'
});
Outlet.hasMany(models.RelocationRequest, {
foreignKey: 'outletId',
as: 'relocationRequests'
});
};
return Outlet;
};

44
models/Region.js Normal file
View File

@ -0,0 +1,44 @@
const { REGIONS } = require('../config/constants');
module.exports = (sequelize, DataTypes) => {
const Region = sequelize.define('Region', {
id: {
type: DataTypes.UUID,
defaultValue: DataTypes.UUIDV4,
primaryKey: true
},
name: {
type: DataTypes.ENUM(Object.values(REGIONS)),
unique: true,
allowNull: false
},
description: {
type: DataTypes.TEXT,
allowNull: true
},
regionalManagerId: {
type: DataTypes.UUID,
allowNull: true,
references: {
model: 'users',
key: 'id'
}
}
}, {
tableName: 'regions',
timestamps: true
});
Region.associate = (models) => {
Region.belongsTo(models.User, {
foreignKey: 'regionalManagerId',
as: 'regionalManager'
});
Region.hasMany(models.Zone, {
foreignKey: 'regionId',
as: 'zones'
});
};
return Region;
};

View File

@ -0,0 +1,99 @@
const { RELOCATION_TYPES, RELOCATION_STAGES } = require('../config/constants');
module.exports = (sequelize, DataTypes) => {
const RelocationRequest = sequelize.define('RelocationRequest', {
id: {
type: DataTypes.UUID,
defaultValue: DataTypes.UUIDV4,
primaryKey: true
},
requestId: {
type: DataTypes.STRING,
unique: true,
allowNull: false
},
outletId: {
type: DataTypes.UUID,
allowNull: false,
references: {
model: 'outlets',
key: 'id'
}
},
dealerId: {
type: DataTypes.UUID,
allowNull: false,
references: {
model: 'users',
key: 'id'
}
},
relocationType: {
type: DataTypes.ENUM(Object.values(RELOCATION_TYPES)),
allowNull: false
},
newAddress: {
type: DataTypes.TEXT,
allowNull: false
},
newCity: {
type: DataTypes.STRING,
allowNull: false
},
newState: {
type: DataTypes.STRING,
allowNull: false
},
reason: {
type: DataTypes.TEXT,
allowNull: false
},
currentStage: {
type: DataTypes.ENUM(Object.values(RELOCATION_STAGES)),
defaultValue: RELOCATION_STAGES.DD_ADMIN_REVIEW
},
status: {
type: DataTypes.STRING,
defaultValue: 'Pending'
},
progressPercentage: {
type: DataTypes.INTEGER,
defaultValue: 0
},
documents: {
type: DataTypes.JSON,
defaultValue: []
},
timeline: {
type: DataTypes.JSON,
defaultValue: []
}
}, {
tableName: 'relocation_requests',
timestamps: true,
indexes: [
{ fields: ['requestId'] },
{ fields: ['outletId'] },
{ fields: ['dealerId'] },
{ fields: ['currentStage'] }
]
});
RelocationRequest.associate = (models) => {
RelocationRequest.belongsTo(models.Outlet, {
foreignKey: 'outletId',
as: 'outlet'
});
RelocationRequest.belongsTo(models.User, {
foreignKey: 'dealerId',
as: 'dealer'
});
RelocationRequest.hasMany(models.Worknote, {
foreignKey: 'requestId',
as: 'worknotes',
scope: { requestType: 'relocation' }
});
};
return RelocationRequest;
};

110
models/Resignation.js Normal file
View File

@ -0,0 +1,110 @@
const { RESIGNATION_TYPES, RESIGNATION_STAGES } = require('../config/constants');
module.exports = (sequelize, DataTypes) => {
const Resignation = sequelize.define('Resignation', {
id: {
type: DataTypes.UUID,
defaultValue: DataTypes.UUIDV4,
primaryKey: true
},
resignationId: {
type: DataTypes.STRING,
unique: true,
allowNull: false
},
outletId: {
type: DataTypes.UUID,
allowNull: false,
references: {
model: 'outlets',
key: 'id'
}
},
dealerId: {
type: DataTypes.UUID,
allowNull: false,
references: {
model: 'users',
key: 'id'
}
},
resignationType: {
type: DataTypes.ENUM(Object.values(RESIGNATION_TYPES)),
allowNull: false
},
lastOperationalDateSales: {
type: DataTypes.DATEONLY,
allowNull: false
},
lastOperationalDateServices: {
type: DataTypes.DATEONLY,
allowNull: false
},
reason: {
type: DataTypes.TEXT,
allowNull: false
},
additionalInfo: {
type: DataTypes.TEXT,
allowNull: true
},
currentStage: {
type: DataTypes.ENUM(Object.values(RESIGNATION_STAGES)),
defaultValue: RESIGNATION_STAGES.ASM
},
status: {
type: DataTypes.STRING,
defaultValue: 'Pending'
},
progressPercentage: {
type: DataTypes.INTEGER,
defaultValue: 0
},
submittedOn: {
type: DataTypes.DATE,
defaultValue: DataTypes.NOW
},
documents: {
type: DataTypes.JSON,
defaultValue: []
},
timeline: {
type: DataTypes.JSON,
defaultValue: []
},
rejectionReason: {
type: DataTypes.TEXT,
allowNull: true
}
}, {
tableName: 'resignations',
timestamps: true,
indexes: [
{ fields: ['resignationId'] },
{ fields: ['outletId'] },
{ fields: ['dealerId'] },
{ fields: ['currentStage'] },
{ fields: ['status'] }
]
});
Resignation.associate = (models) => {
Resignation.belongsTo(models.Outlet, {
foreignKey: 'outletId',
as: 'outlet'
});
Resignation.belongsTo(models.User, {
foreignKey: 'dealerId',
as: 'dealer'
});
Resignation.hasMany(models.Worknote, {
foreignKey: 'requestId',
as: 'worknotes',
scope: {
requestType: 'resignation'
}
});
};
return Resignation;
};

89
models/User.js Normal file
View File

@ -0,0 +1,89 @@
const { ROLES, REGIONS } = require('../config/constants');
module.exports = (sequelize, DataTypes) => {
const User = sequelize.define('User', {
id: {
type: DataTypes.UUID,
defaultValue: DataTypes.UUIDV4,
primaryKey: true
},
email: {
type: DataTypes.STRING,
unique: true,
allowNull: false,
validate: {
isEmail: true
}
},
password: {
type: DataTypes.STRING,
allowNull: false
},
name: {
type: DataTypes.STRING,
allowNull: false
},
role: {
type: DataTypes.ENUM(Object.values(ROLES)),
allowNull: false
},
region: {
type: DataTypes.ENUM(Object.values(REGIONS)),
allowNull: true
},
zone: {
type: DataTypes.STRING,
allowNull: true
},
phone: {
type: DataTypes.STRING,
allowNull: true
},
status: {
type: DataTypes.ENUM('active', 'inactive'),
defaultValue: 'active'
},
lastLogin: {
type: DataTypes.DATE,
allowNull: true
}
}, {
tableName: 'users',
timestamps: true,
indexes: [
{ fields: ['email'] },
{ fields: ['role'] },
{ fields: ['region'] },
{ fields: ['zone'] }
]
});
User.associate = (models) => {
User.hasMany(models.Application, {
foreignKey: 'submittedBy',
as: 'applications'
});
User.hasMany(models.Outlet, {
foreignKey: 'dealerId',
as: 'outlets'
});
User.hasMany(models.Resignation, {
foreignKey: 'dealerId',
as: 'resignations'
});
User.hasMany(models.ConstitutionalChange, {
foreignKey: 'dealerId',
as: 'constitutionalChanges'
});
User.hasMany(models.RelocationRequest, {
foreignKey: 'dealerId',
as: 'relocationRequests'
});
User.hasMany(models.AuditLog, {
foreignKey: 'userId',
as: 'auditLogs'
});
};
return User;
};

52
models/Worknote.js Normal file
View File

@ -0,0 +1,52 @@
const { REQUEST_TYPES } = require('../config/constants');
module.exports = (sequelize, DataTypes) => {
const Worknote = sequelize.define('Worknote', {
id: {
type: DataTypes.UUID,
defaultValue: DataTypes.UUIDV4,
primaryKey: true
},
requestId: {
type: DataTypes.UUID,
allowNull: false
},
requestType: {
type: DataTypes.ENUM(Object.values(REQUEST_TYPES)),
allowNull: false
},
userId: {
type: DataTypes.UUID,
allowNull: false,
references: {
model: 'users',
key: 'id'
}
},
content: {
type: DataTypes.TEXT,
allowNull: false
},
isInternal: {
type: DataTypes.BOOLEAN,
defaultValue: true
}
}, {
tableName: 'worknotes',
timestamps: true,
indexes: [
{ fields: ['requestId'] },
{ fields: ['requestType'] },
{ fields: ['userId'] }
]
});
Worknote.associate = (models) => {
Worknote.belongsTo(models.User, {
foreignKey: 'userId',
as: 'author'
});
};
return Worknote;
};

49
models/Zone.js Normal file
View File

@ -0,0 +1,49 @@
module.exports = (sequelize, DataTypes) => {
const Zone = sequelize.define('Zone', {
id: {
type: DataTypes.UUID,
defaultValue: DataTypes.UUIDV4,
primaryKey: true
},
name: {
type: DataTypes.STRING,
allowNull: false
},
regionId: {
type: DataTypes.UUID,
allowNull: false,
references: {
model: 'regions',
key: 'id'
}
},
zonalManagerId: {
type: DataTypes.UUID,
allowNull: true,
references: {
model: 'users',
key: 'id'
}
}
}, {
tableName: 'zones',
timestamps: true,
indexes: [
{ fields: ['regionId'] },
{ unique: true, fields: ['name', 'regionId'] }
]
});
Zone.associate = (models) => {
Zone.belongsTo(models.Region, {
foreignKey: 'regionId',
as: 'region'
});
Zone.belongsTo(models.User, {
foreignKey: 'zonalManagerId',
as: 'zonalManager'
});
};
return Zone;
};

49
models/index.js Normal file
View File

@ -0,0 +1,49 @@
const { Sequelize } = require('sequelize');
const config = require('../config/database');
const env = process.env.NODE_ENV || 'development';
const dbConfig = config[env];
// Initialize Sequelize
const sequelize = new Sequelize(
dbConfig.database,
dbConfig.username,
dbConfig.password,
{
host: dbConfig.host,
port: dbConfig.port,
dialect: dbConfig.dialect,
logging: dbConfig.logging,
pool: dbConfig.pool,
dialectOptions: dbConfig.dialectOptions
}
);
const db = {};
// Import models
db.User = require('./User')(sequelize, Sequelize.DataTypes);
db.Application = require('./Application')(sequelize, Sequelize.DataTypes);
db.Resignation = require('./Resignation')(sequelize, Sequelize.DataTypes);
db.ConstitutionalChange = require('./ConstitutionalChange')(sequelize, Sequelize.DataTypes);
db.RelocationRequest = require('./RelocationRequest')(sequelize, Sequelize.DataTypes);
db.Outlet = require('./Outlet')(sequelize, Sequelize.DataTypes);
db.Worknote = require('./Worknote')(sequelize, Sequelize.DataTypes);
db.Document = require('./Document')(sequelize, Sequelize.DataTypes);
db.AuditLog = require('./AuditLog')(sequelize, Sequelize.DataTypes);
db.FinancePayment = require('./FinancePayment')(sequelize, Sequelize.DataTypes);
db.FnF = require('./FnF')(sequelize, Sequelize.DataTypes);
db.Region = require('./Region')(sequelize, Sequelize.DataTypes);
db.Zone = require('./Zone')(sequelize, Sequelize.DataTypes);
// Define associations
Object.keys(db).forEach(modelName => {
if (db[modelName].associate) {
db[modelName].associate(db);
}
});
db.sequelize = sequelize;
db.Sequelize = Sequelize;
module.exports = db;

6039
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

49
package.json Normal file
View File

@ -0,0 +1,49 @@
{
"name": "royal-enfield-onboarding-backend",
"version": "1.0.0",
"description": "Backend API for Royal Enfield Dealership Onboarding System",
"main": "server.js",
"scripts": {
"start": "node server.js",
"dev": "nodemon server.js",
"migrate": "node scripts/migrate.js",
"test": "jest",
"test:coverage": "jest --coverage",
"clear-logs": "rm -rf logs/*.log"
},
"keywords": [
"royal-enfield",
"dealership",
"onboarding",
"api"
],
"author": "Royal Enfield",
"license": "PROPRIETARY",
"dependencies": {
"express": "^4.18.2",
"sequelize": "^6.35.2",
"pg": "^8.11.3",
"pg-hstore": "^2.3.4",
"jsonwebtoken": "^9.0.2",
"bcryptjs": "^2.4.3",
"cors": "^2.8.5",
"helmet": "^7.1.0",
"express-validator": "^7.0.1",
"multer": "^1.4.5-lts.1",
"nodemailer": "^6.9.7",
"winston": "^3.11.0",
"dotenv": "^16.3.1",
"uuid": "^9.0.1",
"express-rate-limit": "^7.1.5",
"compression": "^1.7.4"
},
"devDependencies": {
"nodemon": "^3.0.2",
"jest": "^29.7.0",
"supertest": "^6.3.3"
},
"engines": {
"node": ">=18.0.0",
"npm": ">=9.0.0"
}
}

15
routes/applications.js Normal file
View File

@ -0,0 +1,15 @@
const express = require('express');
const router = express.Router();
const applicationController = require('../controllers/applicationController');
const { authenticate, optionalAuth } = require('../middleware/auth');
// Public route - submit application
router.post('/', applicationController.submitApplication);
// Protected routes
router.get('/', authenticate, applicationController.getApplications);
router.get('/:id', authenticate, applicationController.getApplicationById);
router.put('/:id/action', authenticate, applicationController.takeAction);
router.post('/:id/documents', authenticate, applicationController.uploadDocuments);
module.exports = router;

15
routes/auth.js Normal file
View File

@ -0,0 +1,15 @@
const express = require('express');
const router = express.Router();
const authController = require('../controllers/authController');
const { authenticate } = require('../middleware/auth');
// Public routes
router.post('/register', authController.register);
router.post('/login', authController.login);
// Protected routes
router.get('/profile', authenticate, authController.getProfile);
router.put('/profile', authenticate, authController.updateProfile);
router.post('/change-password', authenticate, authController.changePassword);
module.exports = router;

24
routes/constitutional.js Normal file
View File

@ -0,0 +1,24 @@
const express = require('express');
const router = express.Router();
const constitutionalController = require('../controllers/constitutionalController');
const { authenticate } = require('../middleware/auth');
// All routes require authentication
router.use(authenticate);
// Submit constitutional change request
router.post('/', constitutionalController.submitRequest);
// Get constitutional change requests
router.get('/', constitutionalController.getRequests);
// Get specific request details
router.get('/:id', constitutionalController.getRequestById);
// Take action on request
router.put('/:id/action', constitutionalController.takeAction);
// Upload documents
router.post('/:id/documents', constitutionalController.uploadDocuments);
module.exports = router;

16
routes/finance.js Normal file
View File

@ -0,0 +1,16 @@
const express = require('express');
const router = express.Router();
const financeController = require('../controllers/financeController');
const { authenticate } = require('../middleware/auth');
const { checkRole, ROLES } = require('../middleware/roleCheck');
// All routes require authentication
router.use(authenticate);
// Finance user only routes
router.get('/onboarding', checkRole([ROLES.FINANCE, ROLES.SUPER_ADMIN]), financeController.getOnboardingPayments);
router.get('/fnf', checkRole([ROLES.FINANCE, ROLES.SUPER_ADMIN]), financeController.getFnFSettlements);
router.put('/payments/:id', checkRole([ROLES.FINANCE, ROLES.SUPER_ADMIN]), financeController.updatePayment);
router.put('/fnf/:id', checkRole([ROLES.FINANCE, ROLES.SUPER_ADMIN]), financeController.updateFnF);
module.exports = router;

20
routes/master.js Normal file
View File

@ -0,0 +1,20 @@
const express = require('express');
const router = express.Router();
const masterController = require('../controllers/masterController');
const { authenticate } = require('../middleware/auth');
const { checkRole, ROLES } = require('../middleware/roleCheck');
// All routes require authentication
router.use(authenticate);
// Regions
router.get('/regions', masterController.getRegions);
router.post('/regions', checkRole([ROLES.SUPER_ADMIN, ROLES.DD_ADMIN, ROLES.DD_LEAD]), masterController.createRegion);
router.put('/regions/:id', checkRole([ROLES.SUPER_ADMIN, ROLES.DD_ADMIN]), masterController.updateRegion);
// Zones
router.get('/zones', masterController.getZones);
router.post('/zones', checkRole([ROLES.SUPER_ADMIN, ROLES.DD_ADMIN, ROLES.DD_LEAD]), masterController.createZone);
router.put('/zones/:id', checkRole([ROLES.SUPER_ADMIN, ROLES.DD_ADMIN]), masterController.updateZone);
module.exports = router;

21
routes/outlets.js Normal file
View File

@ -0,0 +1,21 @@
const express = require('express');
const router = express.Router();
const outletController = require('../controllers/outletController');
const { authenticate } = require('../middleware/auth');
// All routes require authentication
router.use(authenticate);
// Get all outlets for logged-in dealer
router.get('/', outletController.getOutlets);
// Get specific outlet details
router.get('/:id', outletController.getOutletById);
// Create new outlet (admin only)
router.post('/', outletController.createOutlet);
// Update outlet
router.put('/:id', outletController.updateOutlet);
module.exports = router;

24
routes/relocation.js Normal file
View File

@ -0,0 +1,24 @@
const express = require('express');
const router = express.Router();
const relocationController = require('../controllers/relocationController');
const { authenticate } = require('../middleware/auth');
// All routes require authentication
router.use(authenticate);
// Submit relocation request
router.post('/', relocationController.submitRequest);
// Get relocation requests
router.get('/', relocationController.getRequests);
// Get specific request details
router.get('/:id', relocationController.getRequestById);
// Take action on request
router.put('/:id/action', relocationController.takeAction);
// Upload documents
router.post('/:id/documents', relocationController.uploadDocuments);
module.exports = router;

62
routes/resignations.js Normal file
View File

@ -0,0 +1,62 @@
const express = require('express');
const router = express.Router();
const resignationController = require('../controllers/resignationController');
const { authenticate } = require('../middleware/auth');
const { checkRole } = require('../middleware/roleCheck');
const { ROLES } = require('../config/constants');
// Create resignation (Dealer only)
router.post(
'/create',
authenticate,
checkRole([ROLES.DEALER]),
resignationController.createResignation
);
// Get resignations list (role-based filtering)
router.get(
'/list',
authenticate,
resignationController.getResignations
);
// Get resignation by ID
router.get(
'/:id',
authenticate,
resignationController.getResignationById
);
// Approve resignation (specific roles only)
router.post(
'/:id/approve',
authenticate,
checkRole([
ROLES.RBM,
ROLES.ZBH,
ROLES.NBH,
ROLES.DD_ADMIN,
ROLES.LEGAL_ADMIN,
ROLES.FINANCE,
ROLES.SUPER_ADMIN
]),
resignationController.approveResignation
);
// Reject resignation (specific roles only)
router.post(
'/:id/reject',
authenticate,
checkRole([
ROLES.RBM,
ROLES.ZBH,
ROLES.NBH,
ROLES.DD_ADMIN,
ROLES.LEGAL_ADMIN,
ROLES.FINANCE,
ROLES.SUPER_ADMIN
]),
resignationController.rejectResignation
);
module.exports = router;

65
routes/upload.js Normal file
View File

@ -0,0 +1,65 @@
const express = require('express');
const router = express.Router();
const { authenticate } = require('../middleware/auth');
const { uploadSingle, uploadMultiple, handleUploadError } = require('../middleware/upload');
// All routes require authentication
router.use(authenticate);
// Single file upload
router.post('/document', (req, res, next) => {
uploadSingle(req, res, (err) => {
if (err) {
return handleUploadError(err, req, res, next);
}
if (!req.file) {
return res.status(400).json({
success: false,
message: 'No file uploaded'
});
}
res.json({
success: true,
message: 'File uploaded successfully',
file: {
filename: req.file.filename,
originalName: req.file.originalname,
size: req.file.size,
url: `/uploads/${req.body.uploadType || 'documents'}/${req.file.filename}`
}
});
});
});
// Multiple files upload
router.post('/documents', (req, res, next) => {
uploadMultiple(req, res, (err) => {
if (err) {
return handleUploadError(err, req, res, next);
}
if (!req.files || req.files.length === 0) {
return res.status(400).json({
success: false,
message: 'No files uploaded'
});
}
const files = req.files.map(file => ({
filename: file.filename,
originalName: file.originalname,
size: file.size,
url: `/uploads/${req.body.uploadType || 'documents'}/${file.filename}`
}));
res.json({
success: true,
message: 'Files uploaded successfully',
files
});
});
});
module.exports = router;

18
routes/worknotes.js Normal file
View File

@ -0,0 +1,18 @@
const express = require('express');
const router = express.Router();
const worknoteController = require('../controllers/worknoteController');
const { authenticate } = require('../middleware/auth');
// All routes require authentication
router.use(authenticate);
// Add worknote to a request
router.post('/', worknoteController.addWorknote);
// Get worknotes for a request
router.get('/:requestId', worknoteController.getWorknotes);
// Delete worknote (admin only)
router.delete('/:id', worknoteController.deleteWorknote);
module.exports = router;

40
scripts/migrate.js Normal file
View File

@ -0,0 +1,40 @@
/**
* Database Migration Script
* Synchronizes all Sequelize models with the database
* This script will DROP all existing tables and recreate them.
*
* Run: node scripts/migrate.js
*/
require('dotenv').config();
const db = require('../src/database/models');
async function runMigrations() {
console.log('🔄 Starting database synchronization (Fresh Startup)...\n');
console.log('⚠️ WARNING: This will drop all existing tables in the database.\n');
try {
// Authenticate with the database
await db.sequelize.authenticate();
console.log('📡 Connected to the database successfully.');
// Synchronize models (force: true drops existing tables)
// This ensures that the schema exactly matches the Sequelize models
await db.sequelize.sync({ force: true });
console.log('\n✅ All tables created and synchronized successfully!');
console.log('----------------------------------------------------');
console.log(`Available Models: ${Object.keys(db).filter(k => k !== 'sequelize' && k !== 'Sequelize').join(', ')}`);
console.log('----------------------------------------------------');
process.exit(0);
} catch (error) {
console.error('\n❌ Migration failed:', error.message);
if (error.stack) {
console.error('\nStack Trace:\n', error.stack);
}
process.exit(1);
}
}
runMigrations();

160
scripts/seed.js Normal file
View File

@ -0,0 +1,160 @@
/**
* Database Seeding Script
* Adds initial test data to the database
*
* Run: node scripts/seed.js
*/
require('dotenv').config();
const bcrypt = require('bcryptjs');
const { query } = require('../config/database');
async function seedDatabase() {
console.log('🌱 Starting database seeding...\n');
try {
// 1. Seed regions
console.log('Adding regions...');
const regions = ['East', 'West', 'North', 'South', 'Central'];
for (const region of regions) {
await query(
`INSERT INTO master_regions (region_name) VALUES ($1) ON CONFLICT (region_name) DO NOTHING`,
[region]
);
}
console.log('✅ Regions added\n');
// 2. Seed zones
console.log('Adding zones...');
const zones = [
{ region: 'West', name: 'Mumbai Zone', code: 'MUM-01' },
{ region: 'West', name: 'Pune Zone', code: 'PUN-01' },
{ region: 'North', name: 'Delhi Zone', code: 'DEL-01' },
{ region: 'South', name: 'Bangalore Zone', code: 'BLR-01' },
{ region: 'East', name: 'Kolkata Zone', code: 'KOL-01' },
];
for (const zone of zones) {
const regionResult = await query('SELECT id FROM master_regions WHERE region_name = $1', [zone.region]);
if (regionResult.rows.length > 0) {
await query(
`INSERT INTO master_zones (region_id, zone_name, zone_code)
VALUES ($1, $2, $3) ON CONFLICT (zone_code) DO NOTHING`,
[regionResult.rows[0].id, zone.name, zone.code]
);
}
}
console.log('✅ Zones added\n');
// 3. Seed users
console.log('Adding users...');
const hashedPassword = await bcrypt.hash('Password@123', 10);
const users = [
{ email: 'admin@royalenfield.com', name: 'Super Admin', role: 'Super Admin', region: null, zone: null },
{ email: 'ddlead@royalenfield.com', name: 'DD Lead', role: 'DD Lead', region: 'West', zone: null },
{ email: 'ddhead@royalenfield.com', name: 'DD Head', role: 'DD Head', region: 'West', zone: null },
{ email: 'nbh@royalenfield.com', name: 'NBH', role: 'NBH', region: null, zone: null },
{ email: 'finance@royalenfield.com', name: 'Finance Admin', role: 'Finance', region: null, zone: null },
{ email: 'legal@royalenfield.com', name: 'Legal Admin', role: 'Legal Admin', region: null, zone: null },
{ email: 'dd@royalenfield.com', name: 'DD Mumbai', role: 'DD', region: 'West', zone: 'Mumbai Zone' },
{ email: 'rbm@royalenfield.com', name: 'RBM West', role: 'RBM', region: 'West', zone: 'Mumbai Zone' },
{ email: 'zbh@royalenfield.com', name: 'ZBH Mumbai', role: 'ZBH', region: 'West', zone: 'Mumbai Zone' },
{ email: 'dealer@example.com', name: 'Amit Sharma', role: 'Dealer', region: 'West', zone: 'Mumbai Zone' },
];
for (const user of users) {
const result = await query(
`INSERT INTO users (email, password, full_name, role, region, zone, phone)
VALUES ($1, $2, $3, $4, $5, $6, $7)
ON CONFLICT (email) DO NOTHING
RETURNING id`,
[user.email, hashedPassword, user.name, user.role, user.region, user.zone, '+91-9876543210']
);
if (result.rows.length > 0) {
console.log(` Added: ${user.email} (${user.role})`);
}
}
console.log('✅ Users added\n');
// 4. Seed outlets for dealer
console.log('Adding outlets...');
const dealerResult = await query('SELECT id FROM users WHERE email = $1', ['dealer@example.com']);
if (dealerResult.rows.length > 0) {
const dealerId = dealerResult.rows[0].id;
const outlets = [
{
code: 'DL-MH-001',
name: 'Royal Enfield Mumbai',
type: 'Dealership',
address: 'Plot No. 45, Linking Road, Bandra West',
city: 'Mumbai',
state: 'Maharashtra',
lat: 19.0596,
lon: 72.8295
},
{
code: 'ST-MH-002',
name: 'Royal Enfield Andheri Studio',
type: 'Studio',
address: 'Shop 12, Phoenix Market City, Kurla',
city: 'Mumbai',
state: 'Maharashtra',
lat: 19.0822,
lon: 72.8912
},
{
code: 'DL-MH-003',
name: 'Royal Enfield Thane Dealership',
type: 'Dealership',
address: 'Eastern Express Highway, Thane West',
city: 'Thane',
state: 'Maharashtra',
lat: 19.2183,
lon: 72.9781
},
{
code: 'ST-MH-004',
name: 'Royal Enfield Pune Studio',
type: 'Studio',
address: 'FC Road, Deccan Gymkhana',
city: 'Pune',
state: 'Maharashtra',
lat: 18.5204,
lon: 73.8567
}
];
for (const outlet of outlets) {
await query(
`INSERT INTO outlets
(dealer_id, code, name, type, address, city, state, status, established_date, latitude, longitude)
VALUES ($1, $2, $3, $4, $5, $6, $7, 'Active', '2020-01-15', $8, $9)
ON CONFLICT (code) DO NOTHING`,
[dealerId, outlet.code, outlet.name, outlet.type, outlet.address, outlet.city, outlet.state, outlet.lat, outlet.lon]
);
}
console.log('✅ Outlets added\n');
}
console.log('✅ Database seeding completed successfully!');
console.log('\n📝 Test Credentials:');
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
console.log('Email: admin@royalenfield.com');
console.log('Email: dealer@example.com');
console.log('Email: finance@royalenfield.com');
console.log('Email: ddlead@royalenfield.com');
console.log('\nPassword (all users): Password@123');
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n');
process.exit(0);
} catch (error) {
console.error('❌ Seeding failed:', error);
process.exit(1);
}
}
seedDatabase();

156
server.js Normal file
View File

@ -0,0 +1,156 @@
require('dotenv').config();
const express = require('express');
const cors = require('cors');
const helmet = require('helmet');
const compression = require('compression');
const rateLimit = require('express-rate-limit');
const path = require('path');
// Import database
const db = require('./src/database/models');
// Import routes (Modular Monolith Structure)
const authRoutes = require('./src/modules/auth/auth.routes');
const onboardingRoutes = require('./src/modules/onboarding/onboarding.routes');
const selfServiceRoutes = require('./src/modules/self-service/self-service.routes');
const masterRoutes = require('./src/modules/master/master.routes');
const settlementRoutes = require('./src/modules/settlement/settlement.routes');
const collaborationRoutes = require('./src/modules/collaboration/collaboration.routes');
// Import common middleware & utils
const errorHandler = require('./src/common/middleware/errorHandler');
const logger = require('./src/common/utils/logger');
// Initialize Express app
const app = express();
// Security middleware
app.use(helmet());
app.use(cors({
origin: process.env.FRONTEND_URL || 'http://localhost:5173',
credentials: true
}));
// Rate limiting
const limiter = rateLimit({
windowMs: parseInt(process.env.RATE_LIMIT_WINDOW_MS) || 15 * 60 * 1000, // 15 minutes
max: parseInt(process.env.RATE_LIMIT_MAX_REQUESTS) || 100,
message: 'Too many requests from this IP, please try again later.'
});
app.use('/api/', limiter);
// Body parsing middleware
app.use(express.json({ limit: '10mb' }));
app.use(express.urlencoded({ extended: true, limit: '10mb' }));
// Compression
app.use(compression());
// Static files (uploaded documents)
app.use('/uploads', express.static(path.join(__dirname, 'uploads')));
// Request logging
app.use((req, res, next) => {
logger.info(`${req.method} ${req.path}`, {
ip: req.ip,
userAgent: req.get('user-agent')
});
next();
});
// Health check endpoint
app.get('/health', (req, res) => {
res.json({
status: 'OK',
timestamp: new Date().toISOString(),
uptime: process.uptime(),
environment: process.env.NODE_ENV
});
});
// API Routes (Modular)
app.use('/api/auth', authRoutes);
app.use('/api/onboarding', onboardingRoutes);
app.use('/api/self-service', selfServiceRoutes);
app.use('/api/master', masterRoutes);
app.use('/api/settlement', settlementRoutes);
app.use('/api/collaboration', collaborationRoutes);
// Backward Compatibility Aliases
app.use('/api/applications', onboardingRoutes);
app.use('/api/resignations', require('./src/modules/self-service/resignation.routes'));
app.use('/api/constitutional', (req, res, next) => {
// Map /api/constitutional to /api/self-service/constitutional
req.url = '/constitutional' + req.url;
next();
}, selfServiceRoutes);
app.use('/api/relocations', (req, res, next) => {
// Map /api/relocations to /api/self-service/relocation
req.url = '/relocation' + req.url;
next();
}, selfServiceRoutes);
app.use('/api/outlets', require('./src/modules/master/outlet.routes'));
app.use('/api/finance', settlementRoutes);
app.use('/api/worknotes', collaborationRoutes);
// 404 handler
app.use((req, res) => {
res.status(404).json({
success: false,
message: 'Route not found'
});
});
// Global error handler
app.use(errorHandler);
// Database connection and server start
const PORT = process.env.PORT || 5000;
const startServer = async () => {
try {
// Test database connection
await db.sequelize.authenticate();
logger.info('Database connection established successfully');
// Sync database (in development only)
if (process.env.NODE_ENV === 'development') {
await db.sequelize.sync({ alter: false });
logger.info('Database models synchronized');
}
// Start server
app.listen(PORT, () => {
logger.info(`🚀 Server running on port ${PORT}`);
logger.info(`📍 Environment: ${process.env.NODE_ENV}`);
logger.info(`🔗 API Base URL: http://localhost:${PORT}/api`);
});
} catch (error) {
logger.error('Unable to start server:', error);
process.exit(1);
}
};
// Handle unhandled promise rejections
process.on('unhandledRejection', (err) => {
logger.error('Unhandled Promise Rejection:', err);
process.exit(1);
});
// Handle uncaught exceptions
process.on('uncaughtException', (err) => {
logger.error('Uncaught Exception:', err);
process.exit(1);
});
// Graceful shutdown
process.on('SIGTERM', async () => {
logger.info('SIGTERM signal received: closing HTTP server');
await db.sequelize.close();
process.exit(0);
});
startServer();
module.exports = app;

111
services/auditService.js Normal file
View File

@ -0,0 +1,111 @@
const { query } = require('../config/database');
/**
* Log audit trail for all important actions
*/
const logAudit = async ({ userId, action, entityType, entityId, oldValue = null, newValue = null, ipAddress = null, userAgent = null }) => {
try {
await query(
`INSERT INTO audit_logs
(user_id, action, entity_type, entity_id, old_value, new_value, ip_address, user_agent)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)`,
[
userId,
action,
entityType,
entityId,
oldValue ? JSON.stringify(oldValue) : null,
newValue ? JSON.stringify(newValue) : null,
ipAddress,
userAgent
]
);
console.log(`Audit logged: ${action} by user ${userId}`);
} catch (error) {
console.error('Error logging audit:', error);
// Don't throw error - audit logging should not break the main flow
}
};
/**
* Get audit logs for an entity
*/
const getAuditLogs = async (entityType, entityId) => {
try {
const result = await query(
`SELECT al.*, u.full_name as user_name, u.email as user_email
FROM audit_logs al
LEFT JOIN users u ON al.user_id = u.id
WHERE al.entity_type = $1 AND al.entity_id = $2
ORDER BY al.created_at DESC`,
[entityType, entityId]
);
return result.rows;
} catch (error) {
console.error('Error fetching audit logs:', error);
return [];
}
};
/**
* Get all audit logs with filters
*/
const getAllAuditLogs = async ({ userId, action, entityType, startDate, endDate, limit = 100 }) => {
try {
let queryText = `
SELECT al.*, u.full_name as user_name, u.email as user_email
FROM audit_logs al
LEFT JOIN users u ON al.user_id = u.id
WHERE 1=1
`;
const params = [];
let paramCount = 1;
if (userId) {
queryText += ` AND al.user_id = $${paramCount}`;
params.push(userId);
paramCount++;
}
if (action) {
queryText += ` AND al.action = $${paramCount}`;
params.push(action);
paramCount++;
}
if (entityType) {
queryText += ` AND al.entity_type = $${paramCount}`;
params.push(entityType);
paramCount++;
}
if (startDate) {
queryText += ` AND al.created_at >= $${paramCount}`;
params.push(startDate);
paramCount++;
}
if (endDate) {
queryText += ` AND al.created_at <= $${paramCount}`;
params.push(endDate);
paramCount++;
}
queryText += ` ORDER BY al.created_at DESC LIMIT $${paramCount}`;
params.push(limit);
const result = await query(queryText, params);
return result.rows;
} catch (error) {
console.error('Error fetching all audit logs:', error);
return [];
}
};
module.exports = {
logAudit,
getAuditLogs,
getAllAuditLogs
};

34
src/common/config/auth.js Normal file
View File

@ -0,0 +1,34 @@
const jwt = require('jsonwebtoken');
const JWT_SECRET = process.env.JWT_SECRET || 'your-secret-key-change-in-production';
const JWT_EXPIRE = process.env.JWT_EXPIRE || '7d';
// Generate JWT token
const generateToken = (user) => {
const payload = {
userId: user.id,
email: user.email,
role: user.role,
region: user.region,
zone: user.zone
};
return jwt.sign(payload, JWT_SECRET, {
expiresIn: JWT_EXPIRE
});
};
// Verify JWT token
const verifyToken = (token) => {
try {
return jwt.verify(token, JWT_SECRET);
} catch (error) {
throw new Error('Invalid or expired token');
}
};
module.exports = {
generateToken,
verifyToken,
JWT_SECRET
};

View File

@ -0,0 +1,209 @@
// User Roles
const ROLES = {
DD: 'DD',
DD_ZM: 'DD-ZM',
RBM: 'RBM',
ZBH: 'ZBH',
DD_LEAD: 'DD Lead',
DD_HEAD: 'DD Head',
NBH: 'NBH',
DD_ADMIN: 'DD Admin',
LEGAL_ADMIN: 'Legal Admin',
SUPER_ADMIN: 'Super Admin',
DD_AM: 'DD AM',
FINANCE: 'Finance',
DEALER: 'Dealer'
};
// Regions
const REGIONS = {
EAST: 'East',
WEST: 'West',
NORTH: 'North',
SOUTH: 'South',
CENTRAL: 'Central'
};
// Application Stages
const APPLICATION_STAGES = {
DD: 'DD',
DD_ZM: 'DD-ZM',
RBM: 'RBM',
ZBH: 'ZBH',
DD_LEAD: 'DD Lead',
DD_HEAD: 'DD Head',
NBH: 'NBH',
LEGAL: 'Legal',
FINANCE: 'Finance',
APPROVED: 'Approved',
REJECTED: 'Rejected'
};
// Application Status
const APPLICATION_STATUS = {
PENDING: 'Pending',
IN_REVIEW: 'In Review',
APPROVED: 'Approved',
REJECTED: 'Rejected'
};
// Resignation Stages
const RESIGNATION_STAGES = {
ASM: 'ASM',
RBM: 'RBM',
ZBH: 'ZBH',
NBH: 'NBH',
DD_ADMIN: 'DD Admin',
LEGAL: 'Legal',
FINANCE: 'Finance',
FNF_INITIATED: 'F&F Initiated',
COMPLETED: 'Completed',
REJECTED: 'Rejected'
};
// Resignation Types
const RESIGNATION_TYPES = {
VOLUNTARY: 'Voluntary',
RETIREMENT: 'Retirement',
HEALTH_ISSUES: 'Health Issues',
BUSINESS_CLOSURE: 'Business Closure',
OTHER: 'Other'
};
// Constitutional Change Types
const CONSTITUTIONAL_CHANGE_TYPES = {
OWNERSHIP_TRANSFER: 'Ownership Transfer',
PARTNERSHIP_CHANGE: 'Partnership Change',
LLP_CONVERSION: 'LLP Conversion',
COMPANY_FORMATION: 'Company Formation',
DIRECTOR_CHANGE: 'Director Change'
};
// Constitutional Change Stages
const CONSTITUTIONAL_STAGES = {
DD_ADMIN_REVIEW: 'DD Admin Review',
LEGAL_REVIEW: 'Legal Review',
NBH_APPROVAL: 'NBH Approval',
FINANCE_CLEARANCE: 'Finance Clearance',
COMPLETED: 'Completed',
REJECTED: 'Rejected'
};
// Relocation Types
const RELOCATION_TYPES = {
WITHIN_CITY: 'Within City',
INTERCITY: 'Intercity',
INTERSTATE: 'Interstate'
};
// Relocation Stages
const RELOCATION_STAGES = {
DD_ADMIN_REVIEW: 'DD Admin Review',
RBM_REVIEW: 'RBM Review',
NBH_APPROVAL: 'NBH Approval',
LEGAL_CLEARANCE: 'Legal Clearance',
COMPLETED: 'Completed',
REJECTED: 'Rejected'
};
// Outlet Types
const OUTLET_TYPES = {
DEALERSHIP: 'Dealership',
STUDIO: 'Studio'
};
// Outlet Status
const OUTLET_STATUS = {
ACTIVE: 'Active',
PENDING_RESIGNATION: 'Pending Resignation',
CLOSED: 'Closed'
};
// Business Types
const BUSINESS_TYPES = {
DEALERSHIP: 'Dealership',
STUDIO: 'Studio'
};
// Payment Types
const PAYMENT_TYPES = {
SECURITY_DEPOSIT: 'Security Deposit',
LICENSE_FEE: 'License Fee',
SETUP_FEE: 'Setup Fee',
OTHER: 'Other'
};
// Payment Status
const PAYMENT_STATUS = {
PENDING: 'Pending',
PAID: 'Paid',
OVERDUE: 'Overdue',
WAIVED: 'Waived'
};
// F&F Status
const FNF_STATUS = {
INITIATED: 'Initiated',
DD_CLEARANCE: 'DD Clearance',
LEGAL_CLEARANCE: 'Legal Clearance',
FINANCE_APPROVAL: 'Finance Approval',
COMPLETED: 'Completed'
};
// Audit Actions
const AUDIT_ACTIONS = {
CREATED: 'CREATED',
UPDATED: 'UPDATED',
APPROVED: 'APPROVED',
REJECTED: 'REJECTED',
DELETED: 'DELETED',
STAGE_CHANGED: 'STAGE_CHANGED',
DOCUMENT_UPLOADED: 'DOCUMENT_UPLOADED',
WORKNOTE_ADDED: 'WORKNOTE_ADDED'
};
// Document Types
const DOCUMENT_TYPES = {
GST_CERTIFICATE: 'GST Certificate',
PAN_CARD: 'PAN Card',
AADHAAR: 'Aadhaar',
PARTNERSHIP_DEED: 'Partnership Deed',
LLP_AGREEMENT: 'LLP Agreement',
INCORPORATION_CERTIFICATE: 'Certificate of Incorporation',
MOA: 'MOA',
AOA: 'AOA',
BOARD_RESOLUTION: 'Board Resolution',
PROPERTY_DOCUMENTS: 'Property Documents',
BANK_STATEMENT: 'Bank Statement',
OTHER: 'Other'
};
// Request Types
const REQUEST_TYPES = {
APPLICATION: 'application',
RESIGNATION: 'resignation',
CONSTITUTIONAL: 'constitutional',
RELOCATION: 'relocation'
};
module.exports = {
ROLES,
REGIONS,
APPLICATION_STAGES,
APPLICATION_STATUS,
RESIGNATION_STAGES,
RESIGNATION_TYPES,
CONSTITUTIONAL_CHANGE_TYPES,
CONSTITUTIONAL_STAGES,
RELOCATION_TYPES,
RELOCATION_STAGES,
OUTLET_TYPES,
OUTLET_STATUS,
BUSINESS_TYPES,
PAYMENT_TYPES,
PAYMENT_STATUS,
FNF_STATUS,
AUDIT_ACTIONS,
DOCUMENT_TYPES,
REQUEST_TYPES
};

View File

@ -0,0 +1,49 @@
require('dotenv').config();
module.exports = {
development: {
username: process.env.DB_USER || 'laxman',
password: process.env.DB_PASSWORD || 'Admin@123',
database: process.env.DB_NAME || 'royal_enfield_onboarding',
host: process.env.DB_HOST || 'localhost',
port: process.env.DB_PORT || 5432,
dialect: 'postgres',
logging: console.log,
pool: {
max: 5,
min: 0,
acquire: 30000,
idle: 10000
}
},
production: {
username: process.env.DB_USER || 'laxman',
password: process.env.DB_PASSWORD || 'Admin@123',
database: process.env.DB_NAME || 'royal_enfield_onboarding',
host: process.env.DB_HOST || 'localhost',
port: process.env.DB_PORT || 5432,
dialect: 'postgres',
logging: false,
pool: {
max: 20,
min: 5,
acquire: 60000,
idle: 10000
},
dialectOptions: {
ssl: process.env.DB_SSL === 'true' ? {
require: true,
rejectUnauthorized: false
} : false
}
},
test: {
username: process.env.DB_USER || 'laxman',
password: process.env.DB_PASSWORD || 'Admin@123',
database: process.env.DB_NAME + '_test' || 'royal_enfield_onboarding_test',
host: process.env.DB_HOST || 'localhost',
port: process.env.DB_PORT || 5432,
dialect: 'postgres',
logging: false
}
};

View File

@ -0,0 +1,100 @@
const jwt = require('jsonwebtoken');
const db = require('../../database/models');
const logger = require('../utils/logger');
const authenticate = async (req, res, next) => {
try {
// Get token from header
const authHeader = req.header('Authorization');
if (!authHeader || !authHeader.startsWith('Bearer ')) {
return res.status(401).json({
success: false,
message: 'Access denied. No token provided.'
});
}
const token = authHeader.replace('Bearer ', '');
// Verify token
const decoded = jwt.verify(token, process.env.JWT_SECRET);
// Find user
const user = await db.User.findByPk(decoded.id, {
attributes: { exclude: ['password'] }
});
if (!user) {
return res.status(401).json({
success: false,
message: 'Invalid token. User not found.'
});
}
if (user.status !== 'active') {
return res.status(401).json({
success: false,
message: 'User account is inactive.'
});
}
// Attach user to request
req.user = user;
req.token = token;
next();
} catch (error) {
logger.error('Authentication error:', error);
if (error.name === 'TokenExpiredError') {
return res.status(401).json({
success: false,
message: 'Token expired'
});
}
if (error.name === 'JsonWebTokenError') {
return res.status(401).json({
success: false,
message: 'Invalid token'
});
}
res.status(500).json({
success: false,
message: 'Authentication failed'
});
}
};
const optionalAuth = async (req, res, next) => {
try {
const authHeader = req.header('Authorization');
if (!authHeader || !authHeader.startsWith('Bearer ')) {
return next();
}
const token = authHeader.replace('Bearer ', '');
const decoded = jwt.verify(token, process.env.JWT_SECRET);
const user = await db.User.findByPk(decoded.id, {
attributes: { exclude: ['password'] }
});
if (user && user.status === 'active') {
req.user = user;
req.token = token;
}
next();
} catch (error) {
// If token is invalid/expired, just proceed without user
next();
}
};
module.exports = {
authenticate,
optionalAuth
};

View File

@ -0,0 +1,76 @@
const logger = require('../utils/logger');
const errorHandler = (err, req, res, next) => {
// Log error
logger.error('Error occurred:', {
message: err.message,
stack: err.stack,
path: req.path,
method: req.method,
ip: req.ip
});
// Sequelize validation errors
if (err.name === 'SequelizeValidationError') {
const errors = err.errors.map(e => ({
field: e.path,
message: e.message
}));
return res.status(400).json({
success: false,
message: 'Validation error',
errors
});
}
// Sequelize unique constraint errors
if (err.name === 'SequelizeUniqueConstraintError') {
return res.status(409).json({
success: false,
message: 'Resource already exists',
field: err.errors[0]?.path
});
}
// JWT errors
if (err.name === 'JsonWebTokenError') {
return res.status(401).json({
success: false,
message: 'Invalid token'
});
}
if (err.name === 'TokenExpiredError') {
return res.status(401).json({
success: false,
message: 'Token expired'
});
}
// Multer file upload errors
if (err.name === 'MulterError') {
if (err.code === 'LIMIT_FILE_SIZE') {
return res.status(413).json({
success: false,
message: 'File too large'
});
}
return res.status(400).json({
success: false,
message: err.message
});
}
// Default error
const statusCode = err.statusCode || 500;
const message = err.message || 'Internal server error';
res.status(statusCode).json({
success: false,
message,
...(process.env.NODE_ENV === 'development' && { stack: err.stack })
});
};
module.exports = errorHandler;

View File

@ -0,0 +1,47 @@
const { ROLES } = require('../config/constants');
const logger = require('../utils/logger');
/**
* Role-based access control middleware
* @param {Array<string>} allowedRoles - Array of roles that can access the route
* @returns {Function} Express middleware function
*/
const checkRole = (allowedRoles) => {
return (req, res, next) => {
try {
// Check if user is authenticated
if (!req.user) {
return res.status(401).json({
success: false,
message: 'Authentication required'
});
}
// Check if user role is in allowed roles
if (!allowedRoles.includes(req.user.role)) {
logger.warn(`Access denied for user ${req.user.email} (${req.user.role}) to route ${req.path}`);
return res.status(403).json({
success: false,
message: 'Access denied. Insufficient permissions.',
requiredRoles: allowedRoles,
yourRole: req.user.role
});
}
// User has required role, proceed
next();
} catch (error) {
logger.error('Role check error:', error);
res.status(500).json({
success: false,
message: 'Authorization check failed'
});
}
};
};
module.exports = {
checkRole,
ROLES
};

View File

@ -0,0 +1,67 @@
const winston = require('winston');
const path = require('path');
const fs = require('fs');
// Create logs directory if it doesn't exist
const logsDir = path.join(__dirname, '../../../logs');
if (!fs.existsSync(logsDir)) {
fs.mkdirSync(logsDir);
}
// Define log format
const logFormat = winston.format.combine(
winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }),
winston.format.errors({ stack: true }),
winston.format.splat(),
winston.format.json()
);
// Console format for development
const consoleFormat = winston.format.combine(
winston.format.colorize(),
winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }),
winston.format.printf(({ level, message, timestamp, ...meta }) => {
return `${timestamp} [${level}]: ${message} ${Object.keys(meta).length ? JSON.stringify(meta, null, 2) : ''}`;
})
);
// Create the logger
const logger = winston.createLogger({
level: process.env.LOG_LEVEL || 'info',
format: logFormat,
transports: [
// Write all logs to combined.log
new winston.transports.File({
filename: path.join(logsDir, 'combined.log'),
maxsize: 5242880, // 5MB
maxFiles: 5
}),
// Write error logs to error.log
new winston.transports.File({
filename: path.join(logsDir, 'error.log'),
level: 'error',
maxsize: 5242880, // 5MB
maxFiles: 5
})
],
// Handle exceptions and rejections
exceptionHandlers: [
new winston.transports.File({
filename: path.join(logsDir, 'exceptions.log')
})
],
rejectionHandlers: [
new winston.transports.File({
filename: path.join(logsDir, 'rejections.log')
})
]
});
// Add console transport in development
if (process.env.NODE_ENV !== 'production') {
logger.add(new winston.transports.Console({
format: consoleFormat
}));
}
module.exports = logger;

View File

@ -0,0 +1,124 @@
const { APPLICATION_STAGES, APPLICATION_STATUS, BUSINESS_TYPES } = require('../../common/config/constants');
module.exports = (sequelize, DataTypes) => {
const Application = sequelize.define('Application', {
id: {
type: DataTypes.UUID,
defaultValue: DataTypes.UUIDV4,
primaryKey: true
},
applicationId: {
type: DataTypes.STRING,
unique: true,
allowNull: false
},
applicantName: {
type: DataTypes.STRING,
allowNull: false
},
email: {
type: DataTypes.STRING,
allowNull: false,
validate: { isEmail: true }
},
phone: {
type: DataTypes.STRING,
allowNull: false
},
businessType: {
type: DataTypes.ENUM(Object.values(BUSINESS_TYPES)),
allowNull: false
},
preferredLocation: {
type: DataTypes.STRING,
allowNull: false
},
city: {
type: DataTypes.STRING,
allowNull: false
},
state: {
type: DataTypes.STRING,
allowNull: false
},
experienceYears: {
type: DataTypes.INTEGER,
allowNull: false
},
investmentCapacity: {
type: DataTypes.STRING,
allowNull: false
},
currentStage: {
type: DataTypes.ENUM(Object.values(APPLICATION_STAGES)),
defaultValue: APPLICATION_STAGES.DD
},
overallStatus: {
type: DataTypes.ENUM(Object.values(APPLICATION_STATUS)),
defaultValue: APPLICATION_STATUS.PENDING
},
progressPercentage: {
type: DataTypes.INTEGER,
defaultValue: 0
},
isShortlisted: {
type: DataTypes.BOOLEAN,
defaultValue: false
},
ddLeadShortlisted: {
type: DataTypes.BOOLEAN,
defaultValue: false
},
assignedTo: {
type: DataTypes.UUID,
allowNull: true,
references: {
model: 'users',
key: 'id'
}
},
submittedBy: {
type: DataTypes.UUID,
allowNull: true,
references: {
model: 'users',
key: 'id'
}
},
documents: {
type: DataTypes.JSON,
defaultValue: []
},
timeline: {
type: DataTypes.JSON,
defaultValue: []
}
}, {
tableName: 'applications',
timestamps: true,
indexes: [
{ fields: ['applicationId'] },
{ fields: ['email'] },
{ fields: ['currentStage'] },
{ fields: ['overallStatus'] }
]
});
Application.associate = (models) => {
Application.belongsTo(models.User, {
foreignKey: 'submittedBy',
as: 'submitter'
});
Application.belongsTo(models.User, {
foreignKey: 'assignedTo',
as: 'assignee'
});
Application.hasMany(models.Document, {
foreignKey: 'requestId',
as: 'uploadedDocuments',
scope: { requestType: 'application' }
});
};
return Application;
};

View File

@ -0,0 +1,65 @@
const { AUDIT_ACTIONS } = require('../../common/config/constants');
module.exports = (sequelize, DataTypes) => {
const AuditLog = sequelize.define('AuditLog', {
id: {
type: DataTypes.UUID,
defaultValue: DataTypes.UUIDV4,
primaryKey: true
},
userId: {
type: DataTypes.UUID,
allowNull: true,
references: {
model: 'users',
key: 'id'
}
},
action: {
type: DataTypes.ENUM(Object.values(AUDIT_ACTIONS)),
allowNull: false
},
entityType: {
type: DataTypes.STRING,
allowNull: false
},
entityId: {
type: DataTypes.UUID,
allowNull: false
},
oldData: {
type: DataTypes.JSON,
allowNull: true
},
newData: {
type: DataTypes.JSON,
allowNull: true
},
ipAddress: {
type: DataTypes.STRING,
allowNull: true
},
userAgent: {
type: DataTypes.STRING,
allowNull: true
}
}, {
tableName: 'audit_logs',
timestamps: true,
indexes: [
{ fields: ['userId'] },
{ fields: ['action'] },
{ fields: ['entityType'] },
{ fields: ['entityId'] }
]
});
AuditLog.associate = (models) => {
AuditLog.belongsTo(models.User, {
foreignKey: 'userId',
as: 'user'
});
};
return AuditLog;
};

View File

@ -0,0 +1,87 @@
const { CONSTITUTIONAL_CHANGE_TYPES, CONSTITUTIONAL_STAGES } = require('../../common/config/constants');
module.exports = (sequelize, DataTypes) => {
const ConstitutionalChange = sequelize.define('ConstitutionalChange', {
id: {
type: DataTypes.UUID,
defaultValue: DataTypes.UUIDV4,
primaryKey: true
},
requestId: {
type: DataTypes.STRING,
unique: true,
allowNull: false
},
outletId: {
type: DataTypes.UUID,
allowNull: false,
references: {
model: 'outlets',
key: 'id'
}
},
dealerId: {
type: DataTypes.UUID,
allowNull: false,
references: {
model: 'users',
key: 'id'
}
},
changeType: {
type: DataTypes.ENUM(Object.values(CONSTITUTIONAL_CHANGE_TYPES)),
allowNull: false
},
description: {
type: DataTypes.TEXT,
allowNull: false
},
currentStage: {
type: DataTypes.ENUM(Object.values(CONSTITUTIONAL_STAGES)),
defaultValue: CONSTITUTIONAL_STAGES.DD_ADMIN_REVIEW
},
status: {
type: DataTypes.STRING,
defaultValue: 'Pending'
},
progressPercentage: {
type: DataTypes.INTEGER,
defaultValue: 0
},
documents: {
type: DataTypes.JSON,
defaultValue: []
},
timeline: {
type: DataTypes.JSON,
defaultValue: []
}
}, {
tableName: 'constitutional_changes',
timestamps: true,
indexes: [
{ fields: ['requestId'] },
{ fields: ['outletId'] },
{ fields: ['dealerId'] },
{ fields: ['currentStage'] }
]
});
ConstitutionalChange.associate = (models) => {
ConstitutionalChange.belongsTo(models.Outlet, {
foreignKey: 'outletId',
as: 'outlet'
});
ConstitutionalChange.belongsTo(models.User, {
foreignKey: 'dealerId',
as: 'dealer'
});
ConstitutionalChange.hasMany(models.Worknote, {
foreignKey: 'requestId',
as: 'worknotes',
scope: { requestType: 'constitutional' }
});
};
return ConstitutionalChange;
};

View File

@ -0,0 +1,64 @@
const { REQUEST_TYPES, DOCUMENT_TYPES } = require('../../common/config/constants');
module.exports = (sequelize, DataTypes) => {
const Document = sequelize.define('Document', {
id: {
type: DataTypes.UUID,
defaultValue: DataTypes.UUIDV4,
primaryKey: true
},
requestId: {
type: DataTypes.UUID,
allowNull: false
},
requestType: {
type: DataTypes.ENUM(Object.values(REQUEST_TYPES)),
allowNull: false
},
documentType: {
type: DataTypes.ENUM(Object.values(DOCUMENT_TYPES)),
allowNull: false
},
fileName: {
type: DataTypes.STRING,
allowNull: false
},
fileUrl: {
type: DataTypes.STRING,
allowNull: false
},
fileSize: {
type: DataTypes.INTEGER,
allowNull: true
},
uploadedBy: {
type: DataTypes.UUID,
allowNull: false,
references: {
model: 'users',
key: 'id'
}
},
status: {
type: DataTypes.STRING,
defaultValue: 'Active'
}
}, {
tableName: 'documents',
timestamps: true,
indexes: [
{ fields: ['requestId'] },
{ fields: ['requestType'] },
{ fields: ['documentType'] }
]
});
Document.associate = (models) => {
Document.belongsTo(models.User, {
foreignKey: 'uploadedBy',
as: 'uploader'
});
};
return Document;
};

View File

@ -0,0 +1,75 @@
const { PAYMENT_TYPES, PAYMENT_STATUS } = require('../../common/config/constants');
module.exports = (sequelize, DataTypes) => {
const FinancePayment = sequelize.define('FinancePayment', {
id: {
type: DataTypes.UUID,
defaultValue: DataTypes.UUIDV4,
primaryKey: true
},
applicationId: {
type: DataTypes.UUID,
allowNull: false,
references: {
model: 'applications',
key: 'id'
}
},
paymentType: {
type: DataTypes.ENUM(Object.values(PAYMENT_TYPES)),
allowNull: false
},
amount: {
type: DataTypes.DECIMAL(15, 2),
allowNull: false
},
paymentStatus: {
type: DataTypes.ENUM(Object.values(PAYMENT_STATUS)),
defaultValue: PAYMENT_STATUS.PENDING
},
transactionId: {
type: DataTypes.STRING,
allowNull: true
},
paymentDate: {
type: DataTypes.DATE,
allowNull: true
},
verifiedBy: {
type: DataTypes.UUID,
allowNull: true,
references: {
model: 'users',
key: 'id'
}
},
verificationDate: {
type: DataTypes.DATE,
allowNull: true
},
remarks: {
type: DataTypes.TEXT,
allowNull: true
}
}, {
tableName: 'finance_payments',
timestamps: true,
indexes: [
{ fields: ['applicationId'] },
{ fields: ['paymentStatus'] }
]
});
FinancePayment.associate = (models) => {
FinancePayment.belongsTo(models.Application, {
foreignKey: 'applicationId',
as: 'application'
});
FinancePayment.belongsTo(models.User, {
foreignKey: 'verifiedBy',
as: 'verifier'
});
};
return FinancePayment;
};

View File

@ -0,0 +1,72 @@
const { FNF_STATUS } = require('../../common/config/constants');
module.exports = (sequelize, DataTypes) => {
const FnF = sequelize.define('FnF', {
id: {
type: DataTypes.UUID,
defaultValue: DataTypes.UUIDV4,
primaryKey: true
},
resignationId: {
type: DataTypes.UUID,
allowNull: true,
references: {
model: 'resignations',
key: 'id'
}
},
outletId: {
type: DataTypes.UUID,
allowNull: false,
references: {
model: 'outlets',
key: 'id'
}
},
status: {
type: DataTypes.ENUM(Object.values(FNF_STATUS)),
defaultValue: FNF_STATUS.INITIATED
},
totalReceivables: {
type: DataTypes.DECIMAL(15, 2),
defaultValue: 0
},
totalPayables: {
type: DataTypes.DECIMAL(15, 2),
defaultValue: 0
},
netAmount: {
type: DataTypes.DECIMAL(15, 2),
defaultValue: 0
},
settlementDate: {
type: DataTypes.DATE,
allowNull: true
},
clearanceDocuments: {
type: DataTypes.JSON,
defaultValue: []
}
}, {
tableName: 'fnf_settlements',
timestamps: true,
indexes: [
{ fields: ['resignationId'] },
{ fields: ['outletId'] },
{ fields: ['status'] }
]
});
FnF.associate = (models) => {
FnF.belongsTo(models.Resignation, {
foreignKey: 'resignationId',
as: 'resignation'
});
FnF.belongsTo(models.Outlet, {
foreignKey: 'outletId',
as: 'outlet'
});
};
return FnF;
};

View File

@ -0,0 +1,63 @@
module.exports = (sequelize, DataTypes) => {
const FnFLineItem = sequelize.define('FnFLineItem', {
id: {
type: DataTypes.UUID,
defaultValue: DataTypes.UUIDV4,
primaryKey: true
},
fnfId: {
type: DataTypes.UUID,
allowNull: false,
references: {
model: 'fnf_settlements',
key: 'id'
}
},
itemType: {
type: DataTypes.ENUM('Payable', 'Receivable', 'Deduction'),
allowNull: false
},
description: {
type: DataTypes.STRING,
allowNull: false
},
department: {
type: DataTypes.STRING,
allowNull: false
},
amount: {
type: DataTypes.DECIMAL(15, 2),
allowNull: false,
defaultValue: 0
},
addedBy: {
type: DataTypes.UUID,
allowNull: true,
references: {
model: 'users',
key: 'id'
}
}
}, {
tableName: 'fnf_line_items',
timestamps: true,
indexes: [
{ fields: ['fnfId'] },
{ fields: ['itemType'] },
{ fields: ['department'] }
]
});
FnFLineItem.associate = (models) => {
FnFLineItem.belongsTo(models.FnF, {
foreignKey: 'fnfId',
as: 'settlement'
});
FnFLineItem.belongsTo(models.User, {
foreignKey: 'addedBy',
as: 'creator'
});
};
return FnFLineItem;
};

View File

@ -0,0 +1,53 @@
module.exports = (sequelize, DataTypes) => {
const Notification = sequelize.define('Notification', {
id: {
type: DataTypes.UUID,
defaultValue: DataTypes.UUIDV4,
primaryKey: true
},
userId: {
type: DataTypes.UUID,
allowNull: false,
references: {
model: 'users',
key: 'id'
}
},
title: {
type: DataTypes.STRING,
allowNull: false
},
message: {
type: DataTypes.TEXT,
allowNull: false
},
type: {
type: DataTypes.STRING, // info, warning, success, error
defaultValue: 'info'
},
link: {
type: DataTypes.STRING,
allowNull: true
},
isRead: {
type: DataTypes.BOOLEAN,
defaultValue: false
}
}, {
tableName: 'notifications',
timestamps: true,
indexes: [
{ fields: ['userId'] },
{ fields: ['isRead'] }
]
});
Notification.associate = (models) => {
Notification.belongsTo(models.User, {
foreignKey: 'userId',
as: 'recipient'
});
};
return Notification;
};

View File

@ -0,0 +1,104 @@
const { OUTLET_TYPES, OUTLET_STATUS, REGIONS } = require('../../common/config/constants');
module.exports = (sequelize, DataTypes) => {
const Outlet = sequelize.define('Outlet', {
id: {
type: DataTypes.UUID,
defaultValue: DataTypes.UUIDV4,
primaryKey: true
},
code: {
type: DataTypes.STRING,
unique: true,
allowNull: false
},
name: {
type: DataTypes.STRING,
allowNull: false
},
type: {
type: DataTypes.ENUM(Object.values(OUTLET_TYPES)),
allowNull: false
},
address: {
type: DataTypes.TEXT,
allowNull: false
},
city: {
type: DataTypes.STRING,
allowNull: false
},
state: {
type: DataTypes.STRING,
allowNull: false
},
pincode: {
type: DataTypes.STRING,
allowNull: false
},
latitude: {
type: DataTypes.DECIMAL(10, 8),
allowNull: true
},
longitude: {
type: DataTypes.DECIMAL(11, 8),
allowNull: true
},
status: {
type: DataTypes.ENUM(Object.values(OUTLET_STATUS)),
defaultValue: OUTLET_STATUS.ACTIVE
},
establishedDate: {
type: DataTypes.DATEONLY,
allowNull: false
},
dealerId: {
type: DataTypes.UUID,
allowNull: false,
references: {
model: 'users',
key: 'id'
}
},
region: {
type: DataTypes.ENUM(Object.values(REGIONS)),
allowNull: false
},
zone: {
type: DataTypes.STRING,
allowNull: false
}
}, {
tableName: 'outlets',
timestamps: true,
indexes: [
{ fields: ['code'] },
{ fields: ['dealerId'] },
{ fields: ['type'] },
{ fields: ['status'] },
{ fields: ['region'] },
{ fields: ['zone'] }
]
});
Outlet.associate = (models) => {
Outlet.belongsTo(models.User, {
foreignKey: 'dealerId',
as: 'dealer'
});
Outlet.hasMany(models.Resignation, {
foreignKey: 'outletId',
as: 'resignations'
});
Outlet.hasMany(models.ConstitutionalChange, {
foreignKey: 'outletId',
as: 'constitutionalChanges'
});
Outlet.hasMany(models.RelocationRequest, {
foreignKey: 'outletId',
as: 'relocationRequests'
});
};
return Outlet;
};

View File

@ -0,0 +1,44 @@
const { REGIONS } = require('../../common/config/constants');
module.exports = (sequelize, DataTypes) => {
const Region = sequelize.define('Region', {
id: {
type: DataTypes.UUID,
defaultValue: DataTypes.UUIDV4,
primaryKey: true
},
name: {
type: DataTypes.ENUM(Object.values(REGIONS)),
unique: true,
allowNull: false
},
description: {
type: DataTypes.TEXT,
allowNull: true
},
regionalManagerId: {
type: DataTypes.UUID,
allowNull: true,
references: {
model: 'users',
key: 'id'
}
}
}, {
tableName: 'regions',
timestamps: true
});
Region.associate = (models) => {
Region.belongsTo(models.User, {
foreignKey: 'regionalManagerId',
as: 'regionalManager'
});
Region.hasMany(models.Zone, {
foreignKey: 'regionId',
as: 'zones'
});
};
return Region;
};

View File

@ -0,0 +1,99 @@
const { RELOCATION_TYPES, RELOCATION_STAGES } = require('../../common/config/constants');
module.exports = (sequelize, DataTypes) => {
const RelocationRequest = sequelize.define('RelocationRequest', {
id: {
type: DataTypes.UUID,
defaultValue: DataTypes.UUIDV4,
primaryKey: true
},
requestId: {
type: DataTypes.STRING,
unique: true,
allowNull: false
},
outletId: {
type: DataTypes.UUID,
allowNull: false,
references: {
model: 'outlets',
key: 'id'
}
},
dealerId: {
type: DataTypes.UUID,
allowNull: false,
references: {
model: 'users',
key: 'id'
}
},
relocationType: {
type: DataTypes.ENUM(Object.values(RELOCATION_TYPES)),
allowNull: false
},
newAddress: {
type: DataTypes.TEXT,
allowNull: false
},
newCity: {
type: DataTypes.STRING,
allowNull: false
},
newState: {
type: DataTypes.STRING,
allowNull: false
},
reason: {
type: DataTypes.TEXT,
allowNull: false
},
currentStage: {
type: DataTypes.ENUM(Object.values(RELOCATION_STAGES)),
defaultValue: RELOCATION_STAGES.DD_ADMIN_REVIEW
},
status: {
type: DataTypes.STRING,
defaultValue: 'Pending'
},
progressPercentage: {
type: DataTypes.INTEGER,
defaultValue: 0
},
documents: {
type: DataTypes.JSON,
defaultValue: []
},
timeline: {
type: DataTypes.JSON,
defaultValue: []
}
}, {
tableName: 'relocation_requests',
timestamps: true,
indexes: [
{ fields: ['requestId'] },
{ fields: ['outletId'] },
{ fields: ['dealerId'] },
{ fields: ['currentStage'] }
]
});
RelocationRequest.associate = (models) => {
RelocationRequest.belongsTo(models.Outlet, {
foreignKey: 'outletId',
as: 'outlet'
});
RelocationRequest.belongsTo(models.User, {
foreignKey: 'dealerId',
as: 'dealer'
});
RelocationRequest.hasMany(models.Worknote, {
foreignKey: 'requestId',
as: 'worknotes',
scope: { requestType: 'relocation' }
});
};
return RelocationRequest;
};

View File

@ -0,0 +1,110 @@
const { RESIGNATION_TYPES, RESIGNATION_STAGES } = require('../../common/config/constants');
module.exports = (sequelize, DataTypes) => {
const Resignation = sequelize.define('Resignation', {
id: {
type: DataTypes.UUID,
defaultValue: DataTypes.UUIDV4,
primaryKey: true
},
resignationId: {
type: DataTypes.STRING,
unique: true,
allowNull: false
},
outletId: {
type: DataTypes.UUID,
allowNull: false,
references: {
model: 'outlets',
key: 'id'
}
},
dealerId: {
type: DataTypes.UUID,
allowNull: false,
references: {
model: 'users',
key: 'id'
}
},
resignationType: {
type: DataTypes.ENUM(Object.values(RESIGNATION_TYPES)),
allowNull: false
},
lastOperationalDateSales: {
type: DataTypes.DATEONLY,
allowNull: false
},
lastOperationalDateServices: {
type: DataTypes.DATEONLY,
allowNull: false
},
reason: {
type: DataTypes.TEXT,
allowNull: false
},
additionalInfo: {
type: DataTypes.TEXT,
allowNull: true
},
currentStage: {
type: DataTypes.ENUM(Object.values(RESIGNATION_STAGES)),
defaultValue: RESIGNATION_STAGES.ASM
},
status: {
type: DataTypes.STRING,
defaultValue: 'Pending'
},
progressPercentage: {
type: DataTypes.INTEGER,
defaultValue: 0
},
submittedOn: {
type: DataTypes.DATE,
defaultValue: DataTypes.NOW
},
documents: {
type: DataTypes.JSON,
defaultValue: []
},
timeline: {
type: DataTypes.JSON,
defaultValue: []
},
rejectionReason: {
type: DataTypes.TEXT,
allowNull: true
}
}, {
tableName: 'resignations',
timestamps: true,
indexes: [
{ fields: ['resignationId'] },
{ fields: ['outletId'] },
{ fields: ['dealerId'] },
{ fields: ['currentStage'] },
{ fields: ['status'] }
]
});
Resignation.associate = (models) => {
Resignation.belongsTo(models.Outlet, {
foreignKey: 'outletId',
as: 'outlet'
});
Resignation.belongsTo(models.User, {
foreignKey: 'dealerId',
as: 'dealer'
});
Resignation.hasMany(models.Worknote, {
foreignKey: 'requestId',
as: 'worknotes',
scope: {
requestType: 'resignation'
}
});
};
return Resignation;
};

View File

@ -0,0 +1,49 @@
module.exports = (sequelize, DataTypes) => {
const SLAConfiguration = sequelize.define('SLAConfiguration', {
id: {
type: DataTypes.UUID,
defaultValue: DataTypes.UUIDV4,
primaryKey: true
},
activityName: {
type: DataTypes.STRING,
allowNull: false
},
ownerRole: {
type: DataTypes.STRING,
allowNull: false
},
tatHours: {
type: DataTypes.INTEGER,
allowNull: false
},
tatUnit: {
type: DataTypes.ENUM('hours', 'days'),
defaultValue: 'days'
},
isActive: {
type: DataTypes.BOOLEAN,
defaultValue: true
}
}, {
tableName: 'sla_configurations',
timestamps: true,
indexes: [
{ fields: ['activityName'] },
{ fields: ['ownerRole'] }
]
});
SLAConfiguration.associate = (models) => {
SLAConfiguration.hasMany(models.SLAReminder, {
foreignKey: 'slaConfigId',
as: 'reminders'
});
SLAConfiguration.hasMany(models.SLAEscalationConfig, {
foreignKey: 'slaConfigId',
as: 'escalationConfigs'
});
};
return SLAConfiguration;
};

View File

@ -0,0 +1,49 @@
module.exports = (sequelize, DataTypes) => {
const SLAEscalationConfig = sequelize.define('SLAEscalationConfig', {
id: {
type: DataTypes.UUID,
defaultValue: DataTypes.UUIDV4,
primaryKey: true
},
slaConfigId: {
type: DataTypes.UUID,
allowNull: false,
references: {
model: 'sla_configurations',
key: 'id'
}
},
level: {
type: DataTypes.INTEGER,
allowNull: false
},
timeValue: {
type: DataTypes.INTEGER,
allowNull: false
},
timeUnit: {
type: DataTypes.ENUM('hours', 'days'),
allowNull: false
},
notifyEmail: {
type: DataTypes.STRING,
allowNull: false,
validate: { isEmail: true }
}
}, {
tableName: 'sla_config_escalations',
timestamps: true,
indexes: [
{ fields: ['slaConfigId'] }
]
});
SLAEscalationConfig.associate = (models) => {
SLAEscalationConfig.belongsTo(models.SLAConfiguration, {
foreignKey: 'slaConfigId',
as: 'slaConfig'
});
};
return SLAEscalationConfig;
};

View File

@ -0,0 +1,44 @@
module.exports = (sequelize, DataTypes) => {
const SLAReminder = sequelize.define('SLAReminder', {
id: {
type: DataTypes.UUID,
defaultValue: DataTypes.UUIDV4,
primaryKey: true
},
slaConfigId: {
type: DataTypes.UUID,
allowNull: false,
references: {
model: 'sla_configurations',
key: 'id'
}
},
timeValue: {
type: DataTypes.INTEGER,
allowNull: false
},
timeUnit: {
type: DataTypes.ENUM('hours', 'days'),
allowNull: false
},
isEnabled: {
type: DataTypes.BOOLEAN,
defaultValue: true
}
}, {
tableName: 'sla_config_reminders',
timestamps: true,
indexes: [
{ fields: ['slaConfigId'] }
]
});
SLAReminder.associate = (models) => {
SLAReminder.belongsTo(models.SLAConfiguration, {
foreignKey: 'slaConfigId',
as: 'slaConfig'
});
};
return SLAReminder;
};

View File

@ -0,0 +1,89 @@
const { ROLES, REGIONS } = require('../../common/config/constants');
module.exports = (sequelize, DataTypes) => {
const User = sequelize.define('User', {
id: {
type: DataTypes.UUID,
defaultValue: DataTypes.UUIDV4,
primaryKey: true
},
email: {
type: DataTypes.STRING,
unique: true,
allowNull: false,
validate: {
isEmail: true
}
},
password: {
type: DataTypes.STRING,
allowNull: false
},
name: {
type: DataTypes.STRING,
allowNull: false
},
role: {
type: DataTypes.ENUM(Object.values(ROLES)),
allowNull: false
},
region: {
type: DataTypes.ENUM(Object.values(REGIONS)),
allowNull: true
},
zone: {
type: DataTypes.STRING,
allowNull: true
},
phone: {
type: DataTypes.STRING,
allowNull: true
},
status: {
type: DataTypes.ENUM('active', 'inactive'),
defaultValue: 'active'
},
lastLogin: {
type: DataTypes.DATE,
allowNull: true
}
}, {
tableName: 'users',
timestamps: true,
indexes: [
{ fields: ['email'] },
{ fields: ['role'] },
{ fields: ['region'] },
{ fields: ['zone'] }
]
});
User.associate = (models) => {
User.hasMany(models.Application, {
foreignKey: 'submittedBy',
as: 'applications'
});
User.hasMany(models.Outlet, {
foreignKey: 'dealerId',
as: 'outlets'
});
User.hasMany(models.Resignation, {
foreignKey: 'dealerId',
as: 'resignations'
});
User.hasMany(models.ConstitutionalChange, {
foreignKey: 'dealerId',
as: 'constitutionalChanges'
});
User.hasMany(models.RelocationRequest, {
foreignKey: 'dealerId',
as: 'relocationRequests'
});
User.hasMany(models.AuditLog, {
foreignKey: 'userId',
as: 'auditLogs'
});
};
return User;
};

View File

@ -0,0 +1,47 @@
module.exports = (sequelize, DataTypes) => {
const WorkflowStageConfig = sequelize.define('WorkflowStageConfig', {
id: {
type: DataTypes.UUID,
defaultValue: DataTypes.UUIDV4,
primaryKey: true
},
stageName: {
type: DataTypes.STRING,
unique: true,
allowNull: false
},
stageOrder: {
type: DataTypes.INTEGER,
allowNull: false
},
colorCode: {
type: DataTypes.STRING,
allowNull: true
},
isParallel: {
type: DataTypes.BOOLEAN,
defaultValue: false
},
defaultEvaluators: {
type: DataTypes.JSON,
defaultValue: []
},
isActive: {
type: DataTypes.BOOLEAN,
defaultValue: true
}
}, {
tableName: 'workflow_stages_config',
timestamps: true,
indexes: [
{ fields: ['stageName'] },
{ fields: ['stageOrder'] }
]
});
WorkflowStageConfig.associate = (models) => {
// Future associations with applications or tasks
};
return WorkflowStageConfig;
};

View File

@ -0,0 +1,52 @@
const { REQUEST_TYPES } = require('../../common/config/constants');
module.exports = (sequelize, DataTypes) => {
const Worknote = sequelize.define('Worknote', {
id: {
type: DataTypes.UUID,
defaultValue: DataTypes.UUIDV4,
primaryKey: true
},
requestId: {
type: DataTypes.UUID,
allowNull: false
},
requestType: {
type: DataTypes.ENUM(Object.values(REQUEST_TYPES)),
allowNull: false
},
userId: {
type: DataTypes.UUID,
allowNull: false,
references: {
model: 'users',
key: 'id'
}
},
content: {
type: DataTypes.TEXT,
allowNull: false
},
isInternal: {
type: DataTypes.BOOLEAN,
defaultValue: true
}
}, {
tableName: 'worknotes',
timestamps: true,
indexes: [
{ fields: ['requestId'] },
{ fields: ['requestType'] },
{ fields: ['userId'] }
]
});
Worknote.associate = (models) => {
Worknote.belongsTo(models.User, {
foreignKey: 'userId',
as: 'author'
});
};
return Worknote;
};

View File

@ -0,0 +1,49 @@
module.exports = (sequelize, DataTypes) => {
const Zone = sequelize.define('Zone', {
id: {
type: DataTypes.UUID,
defaultValue: DataTypes.UUIDV4,
primaryKey: true
},
name: {
type: DataTypes.STRING,
allowNull: false
},
regionId: {
type: DataTypes.UUID,
allowNull: false,
references: {
model: 'regions',
key: 'id'
}
},
zonalManagerId: {
type: DataTypes.UUID,
allowNull: true,
references: {
model: 'users',
key: 'id'
}
}
}, {
tableName: 'zones',
timestamps: true,
indexes: [
{ fields: ['regionId'] },
{ unique: true, fields: ['name', 'regionId'] }
]
});
Zone.associate = (models) => {
Zone.belongsTo(models.Region, {
foreignKey: 'regionId',
as: 'region'
});
Zone.belongsTo(models.User, {
foreignKey: 'zonalManagerId',
as: 'zonalManager'
});
};
return Zone;
};

View File

@ -0,0 +1,55 @@
const { Sequelize } = require('sequelize');
const config = require('../../common/config/database');
const env = process.env.NODE_ENV || 'development';
const dbConfig = config[env];
// Initialize Sequelize
const sequelize = new Sequelize(
dbConfig.database,
dbConfig.username,
dbConfig.password,
{
host: dbConfig.host,
port: dbConfig.port,
dialect: dbConfig.dialect,
logging: dbConfig.logging,
pool: dbConfig.pool,
dialectOptions: dbConfig.dialectOptions
}
);
const db = {};
// Import models
db.User = require('./User')(sequelize, Sequelize.DataTypes);
db.Application = require('./Application')(sequelize, Sequelize.DataTypes);
db.Resignation = require('./Resignation')(sequelize, Sequelize.DataTypes);
db.ConstitutionalChange = require('./ConstitutionalChange')(sequelize, Sequelize.DataTypes);
db.RelocationRequest = require('./RelocationRequest')(sequelize, Sequelize.DataTypes);
db.Outlet = require('./Outlet')(sequelize, Sequelize.DataTypes);
db.Worknote = require('./Worknote')(sequelize, Sequelize.DataTypes);
db.Document = require('./Document')(sequelize, Sequelize.DataTypes);
db.AuditLog = require('./AuditLog')(sequelize, Sequelize.DataTypes);
db.FinancePayment = require('./FinancePayment')(sequelize, Sequelize.DataTypes);
db.FnF = require('./FnF')(sequelize, Sequelize.DataTypes);
db.FnFLineItem = require('./FnFLineItem')(sequelize, Sequelize.DataTypes);
db.Region = require('./Region')(sequelize, Sequelize.DataTypes);
db.Zone = require('./Zone')(sequelize, Sequelize.DataTypes);
db.SLAConfiguration = require('./SLAConfiguration')(sequelize, Sequelize.DataTypes);
db.SLAReminder = require('./SLAReminder')(sequelize, Sequelize.DataTypes);
db.SLAEscalationConfig = require('./SLAEscalationConfig')(sequelize, Sequelize.DataTypes);
db.WorkflowStageConfig = require('./WorkflowStageConfig')(sequelize, Sequelize.DataTypes);
db.Notification = require('./Notification')(sequelize, Sequelize.DataTypes);
// Define associations
Object.keys(db).forEach(modelName => {
if (db[modelName].associate) {
db[modelName].associate(db);
}
});
db.sequelize = sequelize;
db.Sequelize = Sequelize;
module.exports = db;

View File

@ -0,0 +1,269 @@
const bcrypt = require('bcryptjs');
const { User, AuditLog } = require('../../database/models');
const { generateToken } = require('../../common/config/auth');
const { AUDIT_ACTIONS } = require('../../common/config/constants');
// Register new user
exports.register = async (req, res) => {
try {
const { email, password, fullName, role, phone, region, zone } = req.body;
// Validate input
if (!email || !password || !fullName || !role) {
return res.status(400).json({
success: false,
message: 'Email, password, full name, and role are required'
});
}
// Check if user already exists
const existingUser = await User.findOne({ where: { email } });
if (existingUser) {
return res.status(400).json({
success: false,
message: 'User with this email already exists'
});
}
// Hash password
const hashedPassword = await bcrypt.hash(password, 10);
// Insert user
const user = await User.create({
email,
password: hashedPassword,
name: fullName,
role,
phone,
region,
zone
});
// Log audit
await AuditLog.create({
userId: user.id,
action: AUDIT_ACTIONS.CREATED,
entityType: 'user',
entityId: user.id
});
res.status(201).json({
success: true,
message: 'User registered successfully',
userId: user.id
});
} catch (error) {
console.error('Register error:', error);
res.status(500).json({
success: false,
message: 'Error registering user'
});
}
};
// Login
exports.login = async (req, res) => {
try {
const { email, password } = req.body;
// Validate input
if (!email || !password) {
return res.status(400).json({
success: false,
message: 'Email and password are required'
});
}
// Get user
const user = await User.findOne({ where: { email } });
if (!user) {
return res.status(401).json({
success: false,
message: 'Invalid email or password'
});
}
// Check if account is active
if (user.status !== 'active') {
return res.status(403).json({
success: false,
message: 'Account is deactivated'
});
}
// Verify password
const isValidPassword = await bcrypt.compare(password, user.password);
if (!isValidPassword) {
return res.status(401).json({
success: false,
message: 'Invalid email or password'
});
}
// Update last login
await user.update({ lastLogin: new Date() });
// Generate token
const token = generateToken(user);
// Log audit
await AuditLog.create({
userId: user.id,
action: 'user_login',
entityType: 'user',
entityId: user.id
});
res.json({
success: true,
token,
user: {
id: user.id,
email: user.email,
fullName: user.name,
role: user.role,
region: user.region,
zone: user.zone
}
});
} catch (error) {
console.error('Login error:', error);
res.status(500).json({
success: false,
message: 'Error during login'
});
}
};
// Get profile
exports.getProfile = async (req, res) => {
try {
const user = await User.findByPk(req.user.id, {
attributes: ['id', 'email', 'name', 'role', 'region', 'zone', 'phone', 'createdAt']
});
if (!user) {
return res.status(404).json({
success: false,
message: 'User not found'
});
}
res.json({
success: true,
user: {
id: user.id,
email: user.email,
fullName: user.name,
role: user.role,
region: user.region,
zone: user.zone,
phone: user.phone,
createdAt: user.createdAt
}
});
} catch (error) {
console.error('Get profile error:', error);
res.status(500).json({
success: false,
message: 'Error fetching profile'
});
}
};
// Update profile
exports.updateProfile = async (req, res) => {
try {
const { fullName, phone } = req.body;
const user = await User.findByPk(req.user.id);
if (!user) {
return res.status(404).json({
success: false,
message: 'User not found'
});
}
await user.update({
name: fullName || user.name,
phone: phone || user.phone
});
// Log audit
await AuditLog.create({
userId: req.user.id,
action: AUDIT_ACTIONS.UPDATED,
entityType: 'user',
entityId: req.user.id
});
res.json({
success: true,
message: 'Profile updated successfully'
});
} catch (error) {
console.error('Update profile error:', error);
res.status(500).json({
success: false,
message: 'Error updating profile'
});
}
};
// Change password
exports.changePassword = async (req, res) => {
try {
const { currentPassword, newPassword } = req.body;
if (!currentPassword || !newPassword) {
return res.status(400).json({
success: false,
message: 'Current password and new password are required'
});
}
// Get current user
const user = await User.findByPk(req.user.id);
if (!user) {
return res.status(404).json({
success: false,
message: 'User not found'
});
}
// Verify current password
const isValid = await bcrypt.compare(currentPassword, user.password);
if (!isValid) {
return res.status(401).json({
success: false,
message: 'Current password is incorrect'
});
}
// Hash new password
const hashedPassword = await bcrypt.hash(newPassword, 10);
// Update password
await user.update({ password: hashedPassword });
// Log audit
await AuditLog.create({
userId: req.user.id,
action: 'password_changed',
entityType: 'user',
entityId: req.user.id
});
res.json({
success: true,
message: 'Password changed successfully'
});
} catch (error) {
console.error('Change password error:', error);
res.status(500).json({
success: false,
message: 'Error changing password'
});
}
};

View File

@ -0,0 +1,15 @@
const express = require('express');
const router = express.Router();
const authController = require('./auth.controller');
const { authenticate } = require('../../common/middleware/auth');
// Public routes
router.post('/register', authController.register);
router.post('/login', authController.login);
// Protected routes
router.get('/profile', authenticate, authController.getProfile);
router.put('/profile', authenticate, authController.updateProfile);
router.post('/change-password', authenticate, authController.changePassword);
module.exports = router;

View File

@ -0,0 +1,84 @@
const { Worknote, User } = require('../../database/models');
exports.addWorknote = async (req, res) => {
try {
const { requestId, requestType, message, isInternal } = req.body;
if (!requestId || !requestType || !message) {
return res.status(400).json({
success: false,
message: 'Request ID, type, and message are required'
});
}
await Worknote.create({
requestId,
requestType,
userId: req.user.id,
content: message,
isInternal: isInternal || false
});
res.status(201).json({
success: true,
message: 'Worknote added successfully'
});
} catch (error) {
console.error('Add worknote error:', error);
res.status(500).json({ success: false, message: 'Error adding worknote' });
}
};
exports.getWorknotes = async (req, res) => {
try {
const { requestId } = req.params;
const { requestType } = req.query;
const worknotes = await Worknote.findAll({
where: {
requestId,
requestType
},
include: [{
model: User,
as: 'author',
attributes: ['name', 'role']
}],
order: [['createdAt', 'DESC']]
});
res.json({
success: true,
worknotes
});
} catch (error) {
console.error('Get worknotes error:', error);
res.status(500).json({ success: false, message: 'Error fetching worknotes' });
}
};
exports.deleteWorknote = async (req, res) => {
try {
const { id } = req.params;
const worknote = await Worknote.findByPk(id);
if (!worknote) {
return res.status(404).json({ success: false, message: 'Worknote not found' });
}
// Only allow user who created it or admin to delete
if (worknote.userId !== req.user.id && req.user.role !== 'Super Admin') {
return res.status(403).json({ success: false, message: 'Access denied' });
}
await worknote.destroy();
res.json({
success: true,
message: 'Worknote deleted successfully'
});
} catch (error) {
console.error('Delete worknote error:', error);
res.status(500).json({ success: false, message: 'Error deleting worknote' });
}
};

View File

@ -0,0 +1,14 @@
const express = require('express');
const router = express.Router();
const collaborationController = require('./collaboration.controller');
const { authenticate } = require('../../common/middleware/auth');
// All routes require authentication
router.use(authenticate);
// Worknotes routes (mounted at /api/collaboration/worknotes or /api/worknotes)
router.post('/', collaborationController.addWorknote);
router.get('/:requestId', collaborationController.getWorknotes);
router.delete('/:id', collaborationController.deleteWorknote);
module.exports = router;

View File

@ -0,0 +1,121 @@
const { Region, Zone } = require('../../database/models');
exports.getRegions = async (req, res) => {
try {
const regions = await Region.findAll({
order: [['name', 'ASC']]
});
res.json({ success: true, regions });
} catch (error) {
console.error('Get regions error:', error);
res.status(500).json({ success: false, message: 'Error fetching regions' });
}
};
exports.createRegion = async (req, res) => {
try {
const { regionName } = req.body;
if (!regionName) {
return res.status(400).json({ success: false, message: 'Region name is required' });
}
await Region.create({ name: regionName });
res.status(201).json({ success: true, message: 'Region created successfully' });
} catch (error) {
console.error('Create region error:', error);
res.status(500).json({ success: false, message: 'Error creating region' });
}
};
exports.updateRegion = async (req, res) => {
try {
const { id } = req.params;
const { regionName, isActive } = req.body;
const region = await Region.findByPk(id);
if (!region) {
return res.status(404).json({ success: false, message: 'Region not found' });
}
await region.update({
name: regionName || region.name,
updatedAt: new Date()
});
res.json({ success: true, message: 'Region updated successfully' });
} catch (error) {
console.error('Update region error:', error);
res.status(500).json({ success: false, message: 'Error updating region' });
}
};
exports.getZones = async (req, res) => {
try {
const { regionId } = req.query;
const where = {};
if (regionId) {
where.regionId = regionId;
}
const zones = await Zone.findAll({
where,
include: [{
model: Region,
as: 'region',
attributes: ['name']
}],
order: [['name', 'ASC']]
});
res.json({ success: true, zones });
} catch (error) {
console.error('Get zones error:', error);
res.status(500).json({ success: false, message: 'Error fetching zones' });
}
};
exports.createZone = async (req, res) => {
try {
const { regionId, zoneName, zoneCode } = req.body;
if (!regionId || !zoneName) {
return res.status(400).json({ success: false, message: 'Region ID and zone name are required' });
}
await Zone.create({
regionId,
name: zoneName
});
res.status(201).json({ success: true, message: 'Zone created successfully' });
} catch (error) {
console.error('Create zone error:', error);
res.status(500).json({ success: false, message: 'Error creating zone' });
}
};
exports.updateZone = async (req, res) => {
try {
const { id } = req.params;
const { zoneName, zoneCode, isActive } = req.body;
const zone = await Zone.findByPk(id);
if (!zone) {
return res.status(404).json({ success: false, message: 'Zone not found' });
}
await zone.update({
name: zoneName || zone.name,
updatedAt: new Date()
});
res.json({ success: true, message: 'Zone updated successfully' });
} catch (error) {
console.error('Update zone error:', error);
res.status(500).json({ success: false, message: 'Error updating zone' });
}
};

View File

@ -0,0 +1,28 @@
const express = require('express');
const router = express.Router();
const masterController = require('./master.controller');
const outletController = require('./outlet.controller');
const { authenticate } = require('../../common/middleware/auth');
const { checkRole } = require('../../common/middleware/roleCheck');
const { ROLES } = require('../../common/config/constants');
// All routes require authentication
router.use(authenticate);
// Regions
router.get('/regions', masterController.getRegions);
router.post('/regions', checkRole([ROLES.SUPER_ADMIN, ROLES.DD_ADMIN, ROLES.DD_LEAD]), masterController.createRegion);
router.put('/regions/:id', checkRole([ROLES.SUPER_ADMIN, ROLES.DD_ADMIN]), masterController.updateRegion);
// Zones
router.get('/zones', masterController.getZones);
router.post('/zones', checkRole([ROLES.SUPER_ADMIN, ROLES.DD_ADMIN, ROLES.DD_LEAD]), masterController.createZone);
router.put('/zones/:id', checkRole([ROLES.SUPER_ADMIN, ROLES.DD_ADMIN]), masterController.updateZone);
// Outlets
router.get('/outlets', outletController.getOutlets);
router.get('/outlets/:id', outletController.getOutletById);
router.post('/outlets', checkRole([ROLES.SUPER_ADMIN, ROLES.DD_ADMIN]), outletController.createOutlet);
router.put('/outlets/:id', checkRole([ROLES.SUPER_ADMIN, ROLES.DD_ADMIN]), outletController.updateOutlet);
module.exports = router;

View File

@ -0,0 +1,185 @@
const { Outlet, User, Resignation } = require('../../database/models');
const { Op } = require('sequelize');
// Get all outlets for logged-in dealer
exports.getOutlets = async (req, res) => {
try {
const where = {};
if (req.user.role === 'Dealer') {
where.dealerId = req.user.id;
}
const outlets = await Outlet.findAll({
where,
include: [
{
model: User,
as: 'dealer',
attributes: ['name', 'email']
},
{
model: Resignation,
as: 'resignations',
required: false,
where: {
status: { [Op.notIn]: ['Completed', 'Rejected'] }
}
}
],
order: [['createdAt', 'DESC']]
});
res.json({
success: true,
outlets
});
} catch (error) {
console.error('Get outlets error:', error);
res.status(500).json({
success: false,
message: 'Error fetching outlets'
});
}
};
// Get specific outlet details
exports.getOutletById = async (req, res) => {
try {
const { id } = req.params;
const outlet = await Outlet.findByPk(id, {
include: [{
model: User,
as: 'dealer',
attributes: ['name', 'email', 'phone']
}]
});
if (!outlet) {
return res.status(404).json({
success: false,
message: 'Outlet not found'
});
}
// Check if dealer can access this outlet
if (req.user.role === 'Dealer' && outlet.dealerId !== req.user.id) {
return res.status(403).json({
success: false,
message: 'Access denied'
});
}
res.json({
success: true,
outlet
});
} catch (error) {
console.error('Get outlet error:', error);
res.status(500).json({
success: false,
message: 'Error fetching outlet'
});
}
};
// Create new outlet (admin only)
exports.createOutlet = async (req, res) => {
try {
const {
dealerId,
code,
name,
type,
address,
city,
state,
pincode,
establishedDate,
latitude,
longitude
} = req.body;
// Validate required fields
if (!dealerId || !code || !name || !type || !address || !city || !state) {
return res.status(400).json({
success: false,
message: 'Missing required fields'
});
}
// Check if code already exists
const existing = await Outlet.findOne({ where: { code } });
if (existing) {
return res.status(400).json({
success: false,
message: 'Outlet code already exists'
});
}
const outlet = await Outlet.create({
dealerId,
code,
name,
type,
address,
city,
state,
pincode,
establishedDate,
latitude,
longitude
});
res.status(201).json({
success: true,
message: 'Outlet created successfully',
outletId: outlet.id
});
} catch (error) {
console.error('Create outlet error:', error);
res.status(500).json({
success: false,
message: 'Error creating outlet'
});
}
};
// Update outlet
exports.updateOutlet = async (req, res) => {
try {
const { id } = req.params;
const { name, address, city, state, pincode, status, latitude, longitude } = req.body;
const outlet = await Outlet.findByPk(id);
if (!outlet) {
return res.status(404).json({
success: false,
message: 'Outlet not found'
});
}
await outlet.update({
name: name || outlet.name,
address: address || outlet.address,
city: city || outlet.city,
state: state || outlet.state,
pincode: pincode || outlet.pincode,
status: status || outlet.status,
latitude: latitude || outlet.latitude,
longitude: longitude || outlet.longitude,
updatedAt: new Date()
});
res.json({
success: true,
message: 'Outlet updated successfully'
});
} catch (error) {
console.error('Update outlet error:', error);
res.status(500).json({
success: false,
message: 'Error updating outlet'
});
}
};

View File

@ -0,0 +1,16 @@
const express = require('express');
const router = express.Router();
const outletController = require('./outlet.controller');
const { authenticate } = require('../../common/middleware/auth');
const { checkRole } = require('../../common/middleware/roleCheck');
const { ROLES } = require('../../common/config/constants');
// All routes require authentication
router.use(authenticate);
router.get('/', outletController.getOutlets);
router.get('/:id', outletController.getOutletById);
router.post('/', checkRole([ROLES.SUPER_ADMIN, ROLES.DD_ADMIN]), outletController.createOutlet);
router.put('/:id', checkRole([ROLES.SUPER_ADMIN, ROLES.DD_ADMIN]), outletController.updateOutlet);
module.exports = router;

View File

@ -0,0 +1,136 @@
const { Application } = require('../../database/models');
const { v4: uuidv4 } = require('uuid');
const { Op } = require('sequelize');
exports.submitApplication = async (req, res) => {
try {
const {
applicantName, email, phone, businessType, locationType,
preferredLocation, city, state, experienceYears, investmentCapacity
} = req.body;
const applicationId = `APP-${new Date().getFullYear()}-${uuidv4().substring(0, 6).toUpperCase()}`;
const application = await Application.create({
applicationId,
applicantName,
email,
phone,
businessType,
preferredLocation,
city,
state,
experienceYears,
investmentCapacity,
currentStage: 'DD',
overallStatus: 'Pending',
progressPercentage: 0
});
res.status(201).json({
success: true,
message: 'Application submitted successfully',
applicationId: application.applicationId
});
} catch (error) {
console.error('Submit application error:', error);
res.status(500).json({ success: false, message: 'Error submitting application' });
}
};
exports.getApplications = async (req, res) => {
try {
const applications = await Application.findAll({
order: [['createdAt', 'DESC']]
});
res.json({ success: true, applications });
} catch (error) {
console.error('Get applications error:', error);
res.status(500).json({ success: false, message: 'Error fetching applications' });
}
};
exports.getApplicationById = async (req, res) => {
try {
const { id } = req.params;
const application = await Application.findOne({
where: {
[Op.or]: [
{ id },
{ applicationId: id }
]
}
});
if (!application) {
return res.status(404).json({ success: false, message: 'Application not found' });
}
res.json({ success: true, application });
} catch (error) {
console.error('Get application error:', error);
res.status(500).json({ success: false, message: 'Error fetching application' });
}
};
exports.takeAction = async (req, res) => {
try {
const { id } = req.params;
const { action, comments, rating } = req.body;
const application = await Application.findOne({
where: {
[Op.or]: [
{ id },
{ applicationId: id }
]
}
});
if (!application) {
return res.status(404).json({ success: false, message: 'Application not found' });
}
await application.update({
overallStatus: action,
updatedAt: new Date()
});
res.json({ success: true, message: 'Action taken successfully' });
} catch (error) {
console.error('Take action error:', error);
res.status(500).json({ success: false, message: 'Error processing action' });
}
};
exports.uploadDocuments = async (req, res) => {
try {
const { id } = req.params;
const { documents } = req.body;
const application = await Application.findOne({
where: {
[Op.or]: [
{ id },
{ applicationId: id }
]
}
});
if (!application) {
return res.status(404).json({ success: false, message: 'Application not found' });
}
await application.update({
documents: documents,
updatedAt: new Date()
});
res.json({ success: true, message: 'Documents uploaded successfully' });
} catch (error) {
console.error('Upload documents error:', error);
res.status(500).json({ success: false, message: 'Error uploading documents' });
}
};

View File

@ -0,0 +1,15 @@
const express = require('express');
const router = express.Router();
const onboardingController = require('./onboarding.controller');
const { authenticate } = require('../../common/middleware/auth');
// Public route - submit application
router.post('/', onboardingController.submitApplication);
// Protected routes
router.get('/', authenticate, onboardingController.getApplications);
router.get('/:id', authenticate, onboardingController.getApplicationById);
router.put('/:id/action', authenticate, onboardingController.takeAction);
router.post('/:id/documents', authenticate, onboardingController.uploadDocuments);
module.exports = router;

View File

@ -0,0 +1,183 @@
const { ConstitutionalChange, Outlet, User, Worknote } = require('../../database/models');
const { v4: uuidv4 } = require('uuid');
const { Op } = require('sequelize');
exports.submitRequest = async (req, res) => {
try {
const {
outletId, changeType, currentConstitution, proposedConstitution,
reason, effectiveDate, newEntityDetails
} = req.body;
const requestId = `CC-${Date.now()}-${uuidv4().substring(0, 4).toUpperCase()}`;
const request = await ConstitutionalChange.create({
requestId,
outletId,
dealerId: req.user.id,
changeType,
description: reason,
currentConstitution,
proposedConstitution,
effectiveDate,
newEntityDetails: JSON.stringify(newEntityDetails),
currentStage: 'RBM Review',
status: 'Pending',
progressPercentage: 0,
timeline: [{
stage: 'Submitted',
timestamp: new Date(),
user: req.user.name,
action: 'Request submitted'
}]
});
res.status(201).json({
success: true,
message: 'Constitutional change request submitted successfully',
requestId: request.requestId
});
} catch (error) {
console.error('Submit constitutional change error:', error);
res.status(500).json({ success: false, message: 'Error submitting request' });
}
};
exports.getRequests = async (req, res) => {
try {
const where = {};
if (req.user.role === 'Dealer') {
where.dealerId = req.user.id;
}
const requests = await ConstitutionalChange.findAll({
where,
include: [
{
model: Outlet,
as: 'outlet',
attributes: ['code', 'name']
},
{
model: User,
as: 'dealer',
attributes: ['name']
}
],
order: [['createdAt', 'DESC']]
});
res.json({ success: true, requests });
} catch (error) {
console.error('Get constitutional changes error:', error);
res.status(500).json({ success: false, message: 'Error fetching requests' });
}
};
exports.getRequestById = async (req, res) => {
try {
const { id } = req.params;
const request = await ConstitutionalChange.findOne({
where: {
[Op.or]: [
{ id },
{ requestId: id }
]
},
include: [
{
model: Outlet,
as: 'outlet'
},
{
model: User,
as: 'dealer',
attributes: ['name', 'email']
},
{
model: Worknote,
as: 'worknotes'
}
]
});
if (!request) {
return res.status(404).json({ success: false, message: 'Request not found' });
}
res.json({ success: true, request });
} catch (error) {
console.error('Get constitutional change details error:', error);
res.status(500).json({ success: false, message: 'Error fetching details' });
}
};
exports.takeAction = async (req, res) => {
try {
const { id } = req.params;
const { action, comments } = req.body;
const request = await ConstitutionalChange.findOne({
where: {
[Op.or]: [
{ id },
{ requestId: id }
]
}
});
if (!request) {
return res.status(404).json({ success: false, message: 'Request not found' });
}
const timeline = [...request.timeline, {
stage: 'Review',
timestamp: new Date(),
user: req.user.name,
action,
remarks: comments
}];
await request.update({
status: action,
timeline,
updatedAt: new Date()
});
res.json({ success: true, message: `Request ${action.toLowerCase()} successfully` });
} catch (error) {
console.error('Take action error:', error);
res.status(500).json({ success: false, message: 'Error processing action' });
}
};
exports.uploadDocuments = async (req, res) => {
try {
const { id } = req.params;
const { documents } = req.body;
const request = await ConstitutionalChange.findOne({
where: {
[Op.or]: [
{ id },
{ requestId: id }
]
}
});
if (!request) {
return res.status(404).json({ success: false, message: 'Request not found' });
}
await request.update({
documents: documents,
updatedAt: new Date()
});
res.json({ success: true, message: 'Documents uploaded successfully' });
} catch (error) {
console.error('Upload documents error:', error);
res.status(500).json({ success: false, message: 'Error uploading documents' });
}
};

View File

@ -0,0 +1,237 @@
const { RelocationRequest, Outlet, User, Worknote } = require('../../database/models');
const { Op } = require('sequelize');
const { v4: uuidv4 } = require('uuid');
exports.submitRequest = async (req, res) => {
try {
const {
outletId, relocationType, currentAddress, currentCity, currentState,
currentLatitude, currentLongitude, proposedAddress, proposedCity,
proposedState, proposedLatitude, proposedLongitude, reason, proposedDate
} = req.body;
const requestId = `REL-${Date.now()}-${uuidv4().substring(0, 4).toUpperCase()}`;
const request = await RelocationRequest.create({
requestId,
outletId,
dealerId: req.user.id,
relocationType,
currentAddress,
currentCity,
currentState,
currentLatitude,
currentLongitude,
proposedAddress,
proposedCity,
proposedState,
proposedLatitude,
proposedLongitude,
reason,
proposedDate,
currentStage: 'RBM Review',
status: 'Pending',
progressPercentage: 0,
timeline: [{
stage: 'Submitted',
timestamp: new Date(),
user: req.user.name,
action: 'Request submitted'
}]
});
res.status(201).json({
success: true,
message: 'Relocation request submitted successfully',
requestId: request.requestId
});
} catch (error) {
console.error('Submit relocation error:', error);
res.status(500).json({ success: false, message: 'Error submitting request' });
}
};
exports.getRequests = async (req, res) => {
try {
const where = {};
if (req.user.role === 'Dealer') {
where.dealerId = req.user.id;
}
const requests = await RelocationRequest.findAll({
where,
include: [
{
model: Outlet,
as: 'outlet',
attributes: ['code', 'name']
},
{
model: User,
as: 'dealer',
attributes: ['name']
}
],
order: [['createdAt', 'DESC']]
});
res.json({ success: true, requests });
} catch (error) {
console.error('Get relocation requests error:', error);
res.status(500).json({ success: false, message: 'Error fetching requests' });
}
};
exports.getRequestById = async (req, res) => {
try {
const { id } = req.params;
const request = await RelocationRequest.findOne({
where: {
[Op.or]: [
{ id },
{ requestId: id }
]
},
include: [
{
model: Outlet,
as: 'outlet'
},
{
model: User,
as: 'dealer',
attributes: ['name', 'email']
},
{
model: Worknote,
as: 'worknotes',
include: [{
model: User,
as: 'actionedBy',
attributes: ['name']
}],
order: [['createdAt', 'ASC']]
}
]
});
if (!request) {
return res.status(404).json({ success: false, message: 'Request not found' });
}
// Calculate distance between current and proposed location
if (request.currentLatitude && request.proposedLatitude) {
const distance = calculateDistance(
request.currentLatitude, request.currentLongitude,
request.proposedLatitude, request.proposedLongitude
);
request.dataValues.distance = `${distance.toFixed(2)} km`;
}
res.json({ success: true, request });
} catch (error) {
console.error('Get relocation details error:', error);
res.status(500).json({ success: false, message: 'Error fetching details' });
}
};
exports.takeAction = async (req, res) => {
try {
const { id } = req.params;
const { action, comments } = req.body;
const request = await RelocationRequest.findOne({
where: {
[Op.or]: [
{ id },
{ requestId: id }
]
}
});
if (!request) {
return res.status(404).json({ success: false, message: 'Request not found' });
}
// Update status and current_stage based on action
let newStatus = request.status;
let newCurrentStage = request.currentStage;
if (action === 'Approved') {
newStatus = 'Approved';
} else if (action === 'Rejected') {
newStatus = 'Rejected';
} else if (action === 'Forwarded to RBM') {
newCurrentStage = 'RBM Review';
} else if (action === 'Forwarded to ZBM') {
newCurrentStage = 'ZBM Review';
} else if (action === 'Forwarded to HO') {
newCurrentStage = 'HO Review';
}
// Create a worknote entry
await Worknote.create({
requestId: request.id,
stage: newCurrentStage,
action: action,
comments: comments,
actionedBy: req.user.id,
actionedAt: new Date()
});
// Update the request status and current stage
await request.update({
status: newStatus,
currentStage: newCurrentStage,
updatedAt: new Date()
});
res.json({ success: true, message: `Request ${action.toLowerCase()} successfully` });
} catch (error) {
console.error('Take action error:', error);
res.status(500).json({ success: false, message: 'Error processing action' });
}
};
exports.uploadDocuments = async (req, res) => {
try {
const { id } = req.params;
const { documents } = req.body;
const request = await RelocationRequest.findOne({
where: {
[Op.or]: [
{ id },
{ requestId: id }
]
}
});
if (!request) {
return res.status(404).json({ success: false, message: 'Request not found' });
}
await request.update({
documents: documents,
updatedAt: new Date()
});
res.json({ success: true, message: 'Documents uploaded successfully' });
} catch (error) {
console.error('Upload documents error:', error);
res.status(500).json({ success: false, message: 'Error uploading documents' });
}
};
// Helper function to calculate distance between two coordinates
function calculateDistance(lat1, lon1, lat2, lon2) {
const R = 6371; // Radius of Earth in km
const dLat = (lat2 - lat1) * Math.PI / 180;
const dLon = (lon2 - lon1) * Math.PI / 180;
const a = Math.sin(dLat / 2) * Math.sin(dLat / 2) +
Math.cos(lat1 * Math.PI / 180) * Math.cos(lat2 * Math.PI / 180) *
Math.sin(dLon / 2) * Math.sin(dLon / 2);
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
return R * c;
}

View File

@ -0,0 +1,409 @@
const db = require('../../database/models');
const logger = require('../../common/utils/logger');
const { RESIGNATION_STAGES, AUDIT_ACTIONS, ROLES } = require('../../common/config/constants');
const { Op } = require('sequelize');
// Generate unique resignation ID
const generateResignationId = async () => {
const count = await db.Resignation.count();
return `RES-${String(count + 1).padStart(3, '0')}`;
};
// Calculate progress percentage based on stage
const calculateProgress = (stage) => {
const stageProgress = {
[RESIGNATION_STAGES.ASM]: 15,
[RESIGNATION_STAGES.RBM]: 30,
[RESIGNATION_STAGES.ZBH]: 45,
[RESIGNATION_STAGES.NBH]: 60,
[RESIGNATION_STAGES.DD_ADMIN]: 70,
[RESIGNATION_STAGES.LEGAL]: 80,
[RESIGNATION_STAGES.FINANCE]: 90,
[RESIGNATION_STAGES.FNF_INITIATED]: 95,
[RESIGNATION_STAGES.COMPLETED]: 100,
[RESIGNATION_STAGES.REJECTED]: 0
};
return stageProgress[stage] || 0;
};
// Create resignation request (Dealer only)
exports.createResignation = async (req, res, next) => {
const transaction = await db.sequelize.transaction();
try {
const { outletId, resignationType, lastOperationalDateSales, lastOperationalDateServices, reason, additionalInfo } = req.body;
const dealerId = req.user.id;
// Verify outlet belongs to dealer
const outlet = await db.Outlet.findOne({
where: { id: outletId, dealerId }
});
if (!outlet) {
await transaction.rollback();
return res.status(404).json({
success: false,
message: 'Outlet not found or does not belong to you'
});
}
// Check if outlet already has active resignation
const existingResignation = await db.Resignation.findOne({
where: {
outletId,
status: { [Op.notIn]: ['Completed', 'Rejected'] }
}
});
if (existingResignation) {
await transaction.rollback();
return res.status(400).json({
success: false,
message: 'This outlet already has an active resignation request'
});
}
// Generate resignation ID
const resignationId = await generateResignationId();
// Create resignation
const resignation = await db.Resignation.create({
resignationId,
outletId,
dealerId,
resignationType,
lastOperationalDateSales,
lastOperationalDateServices,
reason,
additionalInfo,
currentStage: RESIGNATION_STAGES.ASM,
status: 'ASM Review',
progressPercentage: 15,
timeline: [{
stage: 'Submitted',
timestamp: new Date(),
user: req.user.name,
action: 'Resignation request submitted'
}]
}, { transaction });
// Update outlet status
await outlet.update({
status: 'Pending Resignation'
}, { transaction });
// Create audit log
await db.AuditLog.create({
userId: req.user.id,
userName: req.user.name,
userRole: req.user.role,
action: AUDIT_ACTIONS.CREATED,
entityType: 'resignation',
entityId: resignation.id,
changes: { created: resignation.toJSON() }
}, { transaction });
await transaction.commit();
logger.info(`Resignation request created: ${resignationId} by dealer: ${req.user.email}`);
res.status(201).json({
success: true,
message: 'Resignation request submitted successfully',
resignationId: resignation.resignationId,
resignation: resignation.toJSON()
});
} catch (error) {
if (transaction) await transaction.rollback();
logger.error('Error creating resignation:', error);
next(error);
}
};
// Get resignations list (role-based filtering)
exports.getResignations = async (req, res, next) => {
try {
const { status, region, zone, page = 1, limit = 10 } = req.query;
const offset = (page - 1) * limit;
// Build where clause based on user role
let where = {};
if (req.user.role === ROLES.DEALER) {
// Dealers see only their resignations
where.dealerId = req.user.id;
} else if (req.user.region && ![ROLES.NBH, ROLES.DD_HEAD, ROLES.DD_LEAD, ROLES.SUPER_ADMIN].includes(req.user.role)) {
// Regional users see resignations in their region
where['$outlet.region$'] = req.user.region;
}
if (status) {
where.status = status;
}
// Get resignations
const { count, rows: resignations } = await db.Resignation.findAndCountAll({
where,
include: [
{
model: db.Outlet,
as: 'outlet',
attributes: ['id', 'code', 'name', 'type', 'city', 'state', 'region', 'zone']
},
{
model: db.User,
as: 'dealer',
attributes: ['id', 'name', 'email', 'phone']
}
],
order: [['createdAt', 'DESC']],
limit: parseInt(limit),
offset: parseInt(offset)
});
res.json({
success: true,
resignations,
pagination: {
total: count,
page: parseInt(page),
pages: Math.ceil(count / limit),
limit: parseInt(limit)
}
});
} catch (error) {
logger.error('Error fetching resignations:', error);
next(error);
}
};
// Get resignation details
exports.getResignationById = async (req, res, next) => {
try {
const { id } = req.params;
const resignation = await db.Resignation.findOne({
where: { id },
include: [
{
model: db.Outlet,
as: 'outlet',
include: [
{
model: db.User,
as: 'dealer',
attributes: ['id', 'name', 'email', 'phone']
}
]
},
{
model: db.Worknote,
as: 'worknotes',
order: [['createdAt', 'DESC']]
}
]
});
if (!resignation) {
return res.status(404).json({
success: false,
message: 'Resignation not found'
});
}
// Check access permissions
if (req.user.role === ROLES.DEALER && resignation.dealerId !== req.user.id) {
return res.status(403).json({
success: false,
message: 'Access denied'
});
}
res.json({
success: true,
resignation
});
} catch (error) {
logger.error('Error fetching resignation details:', error);
next(error);
}
};
// Approve resignation (move to next stage)
exports.approveResignation = async (req, res, next) => {
const transaction = await db.sequelize.transaction();
try {
const { id } = req.params;
const { remarks } = req.body;
const resignation = await db.Resignation.findByPk(id, {
include: [{ model: db.Outlet, as: 'outlet' }]
});
if (!resignation) {
await transaction.rollback();
return res.status(404).json({
success: false,
message: 'Resignation not found'
});
}
// Determine next stage based on current stage
const stageFlow = {
[RESIGNATION_STAGES.ASM]: RESIGNATION_STAGES.RBM,
[RESIGNATION_STAGES.RBM]: RESIGNATION_STAGES.ZBH,
[RESIGNATION_STAGES.ZBH]: RESIGNATION_STAGES.NBH,
[RESIGNATION_STAGES.NBH]: RESIGNATION_STAGES.DD_ADMIN,
[RESIGNATION_STAGES.DD_ADMIN]: RESIGNATION_STAGES.LEGAL,
[RESIGNATION_STAGES.LEGAL]: RESIGNATION_STAGES.FINANCE,
[RESIGNATION_STAGES.FINANCE]: RESIGNATION_STAGES.FNF_INITIATED,
[RESIGNATION_STAGES.FNF_INITIATED]: RESIGNATION_STAGES.COMPLETED
};
const nextStage = stageFlow[resignation.currentStage];
if (!nextStage) {
await transaction.rollback();
return res.status(400).json({
success: false,
message: 'Cannot approve from current stage'
});
}
// Update resignation
const timeline = [...resignation.timeline, {
stage: nextStage,
timestamp: new Date(),
user: req.user.name,
action: 'Approved',
remarks
}];
await resignation.update({
currentStage: nextStage,
status: nextStage === RESIGNATION_STAGES.COMPLETED ? 'Completed' : `${nextStage} Review`,
progressPercentage: calculateProgress(nextStage),
timeline
}, { transaction });
// If completed, update outlet status
if (nextStage === RESIGNATION_STAGES.COMPLETED) {
await resignation.outlet.update({
status: 'Closed'
}, { transaction });
}
// Create audit log
await db.AuditLog.create({
userId: req.user.id,
userName: req.user.name,
userRole: req.user.role,
action: AUDIT_ACTIONS.APPROVED,
entityType: 'resignation',
entityId: resignation.id,
changes: {
from: resignation.currentStage,
to: nextStage,
remarks
}
}, { transaction });
await transaction.commit();
logger.info(`Resignation ${resignation.resignationId} approved to ${nextStage} by ${req.user.email}`);
res.json({
success: true,
message: 'Resignation approved successfully',
nextStage,
resignation
});
} catch (error) {
if (transaction) await transaction.rollback();
logger.error('Error approving resignation:', error);
next(error);
}
};
// Reject resignation
exports.rejectResignation = async (req, res, next) => {
const transaction = await db.sequelize.transaction();
try {
const { id } = req.params;
const { reason } = req.body;
if (!reason) {
await transaction.rollback();
return res.status(400).json({
success: false,
message: 'Rejection reason is required'
});
}
const resignation = await db.Resignation.findByPk(id, {
include: [{ model: db.Outlet, as: 'outlet' }]
});
if (!resignation) {
await transaction.rollback();
return res.status(404).json({
success: false,
message: 'Resignation not found'
});
}
// Update resignation
const timeline = [...resignation.timeline, {
stage: 'Rejected',
timestamp: new Date(),
user: req.user.name,
action: 'Rejected',
reason
}];
await resignation.update({
currentStage: RESIGNATION_STAGES.REJECTED,
status: 'Rejected',
progressPercentage: 0,
rejectionReason: reason,
timeline
}, { transaction });
// Update outlet status back to Active
await resignation.outlet.update({
status: 'Active'
}, { transaction });
// Create audit log
await db.AuditLog.create({
userId: req.user.id,
userName: req.user.name,
userRole: req.user.role,
action: AUDIT_ACTIONS.REJECTED,
entityType: 'resignation',
entityId: resignation.id,
changes: { reason }
}, { transaction });
await transaction.commit();
logger.info(`Resignation ${resignation.resignationId} rejected by ${req.user.email}`);
res.json({
success: true,
message: 'Resignation rejected',
resignation
});
} catch (error) {
if (transaction) await transaction.rollback();
logger.error('Error rejecting resignation:', error);
next(error);
}
};

View File

@ -0,0 +1,13 @@
const express = require('express');
const router = express.Router();
const resignationController = require('./resignation.controller');
const { authenticate } = require('../../common/middleware/auth');
// Protected routes
router.post('/', authenticate, resignationController.createResignation);
router.get('/', authenticate, resignationController.getResignations);
router.get('/:id', authenticate, resignationController.getResignationById);
router.put('/:id/approve', authenticate, resignationController.approveResignation);
router.put('/:id/reject', authenticate, resignationController.rejectResignation);
module.exports = router;

View File

@ -0,0 +1,26 @@
const express = require('express');
const router = express.Router();
const resignationRoutes = require('./resignation.routes');
const constitutionalController = require('./constitutional.controller');
const relocationController = require('./relocation.controller');
const { authenticate } = require('../../common/middleware/auth');
// Resignations submodule
router.use('/resignations', resignationRoutes);
// Constitutional changes submodule
router.post('/constitutional', authenticate, constitutionalController.submitRequest);
router.get('/constitutional', authenticate, constitutionalController.getRequests);
router.get('/constitutional/:id', authenticate, constitutionalController.getRequestById);
router.put('/constitutional/:id/action', authenticate, constitutionalController.takeAction);
router.post('/constitutional/:id/documents', authenticate, constitutionalController.uploadDocuments);
// Relocation submodule
router.post('/relocation', authenticate, relocationController.submitRequest);
router.get('/relocation', authenticate, relocationController.getRequests);
router.get('/relocation/:id', authenticate, relocationController.getRequestById);
router.put('/relocation/:id/action', authenticate, relocationController.takeAction);
router.post('/relocation/:id/documents', authenticate, relocationController.uploadDocuments);
module.exports = router;

View File

@ -0,0 +1,99 @@
const { FinancePayment, FnF, Application, Resignation, User, Outlet } = require('../../database/models');
exports.getOnboardingPayments = async (req, res) => {
try {
const payments = await FinancePayment.findAll({
include: [{
model: Application,
as: 'application',
attributes: ['applicantName', 'applicationId']
}],
order: [['createdAt', 'ASC']]
});
res.json({ success: true, payments });
} catch (error) {
console.error('Get onboarding payments error:', error);
res.status(500).json({ success: false, message: 'Error fetching payments' });
}
};
exports.getFnFSettlements = async (req, res) => {
try {
const settlements = await FnF.findAll({
include: [
{
model: Resignation,
as: 'resignation',
attributes: ['resignationId']
},
{
model: Outlet,
as: 'outlet',
include: [{
model: User,
as: 'dealer',
attributes: ['name']
}]
}
],
order: [['createdAt', 'DESC']]
});
res.json({ success: true, settlements });
} catch (error) {
console.error('Get F&F settlements error:', error);
res.status(500).json({ success: false, message: 'Error fetching settlements' });
}
};
exports.updatePayment = async (req, res) => {
try {
const { id } = req.params;
const { paidDate, amount, paymentMode, transactionReference, status } = req.body;
const payment = await FinancePayment.findByPk(id);
if (!payment) {
return res.status(404).json({ success: false, message: 'Payment not found' });
}
await payment.update({
paymentDate: paidDate || payment.paymentDate,
amount: amount || payment.amount,
transactionId: transactionReference || payment.transactionId,
paymentStatus: status || payment.paymentStatus,
updatedAt: new Date()
});
res.json({ success: true, message: 'Payment updated successfully' });
} catch (error) {
console.error('Update payment error:', error);
res.status(500).json({ success: false, message: 'Error updating payment' });
}
};
exports.updateFnF = async (req, res) => {
try {
const { id } = req.params;
const {
inventoryClearance, sparesClearance, accountsClearance, legalClearance,
finalSettlementAmount, status
} = req.body;
const fnf = await FnF.findByPk(id);
if (!fnf) {
return res.status(404).json({ success: false, message: 'F&F settlement not found' });
}
await fnf.update({
status: status || fnf.status,
netAmount: finalSettlementAmount || fnf.netAmount,
updatedAt: new Date()
});
res.json({ success: true, message: 'F&F settlement updated successfully' });
} catch (error) {
console.error('Update F&F error:', error);
res.status(500).json({ success: false, message: 'Error updating F&F settlement' });
}
};

View File

@ -0,0 +1,17 @@
const express = require('express');
const router = express.Router();
const settlementController = require('./settlement.controller');
const { authenticate } = require('../../common/middleware/auth');
const { checkRole } = require('../../common/middleware/roleCheck');
const { ROLES } = require('../../common/config/constants');
// All routes require authentication
router.use(authenticate);
// Finance user only routes
router.get('/onboarding', checkRole([ROLES.FINANCE, ROLES.SUPER_ADMIN]), settlementController.getOnboardingPayments);
router.get('/fnf', checkRole([ROLES.FINANCE, ROLES.SUPER_ADMIN]), settlementController.getFnFSettlements);
router.put('/payments/:id', checkRole([ROLES.FINANCE, ROLES.SUPER_ADMIN]), settlementController.updatePayment);
router.put('/fnf/:id', checkRole([ROLES.FINANCE, ROLES.SUPER_ADMIN]), settlementController.updateFnF);
module.exports = router;

67
utils/logger.js Normal file
View File

@ -0,0 +1,67 @@
const winston = require('winston');
const path = require('path');
const fs = require('fs');
// Create logs directory if it doesn't exist
const logsDir = path.join(__dirname, '../logs');
if (!fs.existsSync(logsDir)) {
fs.mkdirSync(logsDir);
}
// Define log format
const logFormat = winston.format.combine(
winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }),
winston.format.errors({ stack: true }),
winston.format.splat(),
winston.format.json()
);
// Console format for development
const consoleFormat = winston.format.combine(
winston.format.colorize(),
winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }),
winston.format.printf(({ level, message, timestamp, ...meta }) => {
return `${timestamp} [${level}]: ${message} ${Object.keys(meta).length ? JSON.stringify(meta, null, 2) : ''}`;
})
);
// Create the logger
const logger = winston.createLogger({
level: process.env.LOG_LEVEL || 'info',
format: logFormat,
transports: [
// Write all logs to combined.log
new winston.transports.File({
filename: path.join(logsDir, 'combined.log'),
maxsize: 5242880, // 5MB
maxFiles: 5
}),
// Write error logs to error.log
new winston.transports.File({
filename: path.join(logsDir, 'error.log'),
level: 'error',
maxsize: 5242880, // 5MB
maxFiles: 5
})
],
// Handle exceptions and rejections
exceptionHandlers: [
new winston.transports.File({
filename: path.join(logsDir, 'exceptions.log')
})
],
rejectionHandlers: [
new winston.transports.File({
filename: path.join(logsDir, 'rejections.log')
})
]
});
// Add console transport in development
if (process.env.NODE_ENV !== 'production') {
logger.add(new winston.transports.Console({
format: consoleFormat
}));
}
module.exports = logger;