tables added based on the frontend ui and backend is up need to test thouroughly
This commit is contained in:
commit
8984a314a7
137
.gitignore
vendored
Normal file
137
.gitignore
vendored
Normal 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
|
||||
464
AI-ASSISTANT-INSTRUCTIONS.md
Normal file
464
AI-ASSISTANT-INSTRUCTIONS.md
Normal 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
238
README.md
Normal 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! 🚀
|
||||
34
config/auth.js
Normal file
34
config/auth.js
Normal 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
209
config/constants.js
Normal 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
49
config/database.js
Normal 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
12
config/email.js
Normal 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>'
|
||||
};
|
||||
135
controllers/applicationController.js
Normal file
135
controllers/applicationController.js
Normal 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' });
|
||||
}
|
||||
};
|
||||
269
controllers/authController.js
Normal file
269
controllers/authController.js
Normal 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'
|
||||
});
|
||||
}
|
||||
};
|
||||
183
controllers/constitutionalController.js
Normal file
183
controllers/constitutionalController.js
Normal 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' });
|
||||
}
|
||||
};
|
||||
99
controllers/financeController.js
Normal file
99
controllers/financeController.js
Normal 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' });
|
||||
}
|
||||
};
|
||||
121
controllers/masterController.js
Normal file
121
controllers/masterController.js
Normal 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' });
|
||||
}
|
||||
};
|
||||
184
controllers/outletController.js
Normal file
184
controllers/outletController.js
Normal 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'
|
||||
});
|
||||
}
|
||||
};
|
||||
238
controllers/relocationController.js
Normal file
238
controllers/relocationController.js
Normal 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;
|
||||
}
|
||||
417
controllers/resignationController.js
Normal file
417
controllers/resignationController.js
Normal 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;
|
||||
84
controllers/worknoteController.js
Normal file
84
controllers/worknoteController.js
Normal 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' });
|
||||
}
|
||||
};
|
||||
65
docs/Backend_Alignment_Report_v1.4.md
Normal file
65
docs/Backend_Alignment_Report_v1.4.md
Normal 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.
|
||||
56
docs/Comparison_Summary_v1.0_vs_v1.4.md
Normal file
56
docs/Comparison_Summary_v1.0_vs_v1.4.md
Normal 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*
|
||||
7220
docs/Re_New_Dealer_Onboard_TWO.md
Normal file
7220
docs/Re_New_Dealer_Onboard_TWO.md
Normal file
File diff suppressed because it is too large
Load Diff
1189
docs/dealer_onboard_backend_schema.mermaid
Normal file
1189
docs/dealer_onboard_backend_schema.mermaid
Normal file
File diff suppressed because it is too large
Load Diff
100
middleware/auth.js
Normal file
100
middleware/auth.js
Normal 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
|
||||
};
|
||||
76
middleware/errorHandler.js
Normal file
76
middleware/errorHandler.js
Normal 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
47
middleware/roleCheck.js
Normal 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
101
middleware/upload.js
Normal 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
104
models/Application.js
Normal 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
65
models/AuditLog.js
Normal 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;
|
||||
};
|
||||
87
models/ConstitutionalChange.js
Normal file
87
models/ConstitutionalChange.js
Normal 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
64
models/Document.js
Normal 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
75
models/FinancePayment.js
Normal 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
72
models/FnF.js
Normal 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
104
models/Outlet.js
Normal 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
44
models/Region.js
Normal 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;
|
||||
};
|
||||
99
models/RelocationRequest.js
Normal file
99
models/RelocationRequest.js
Normal 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
110
models/Resignation.js
Normal 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
89
models/User.js
Normal 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
52
models/Worknote.js
Normal 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
49
models/Zone.js
Normal 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
49
models/index.js
Normal 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
6039
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
49
package.json
Normal file
49
package.json
Normal 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
15
routes/applications.js
Normal 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
15
routes/auth.js
Normal 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
24
routes/constitutional.js
Normal 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
16
routes/finance.js
Normal 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
20
routes/master.js
Normal 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
21
routes/outlets.js
Normal 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
24
routes/relocation.js
Normal 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
62
routes/resignations.js
Normal 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
65
routes/upload.js
Normal 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
18
routes/worknotes.js
Normal 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
40
scripts/migrate.js
Normal 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
160
scripts/seed.js
Normal 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
156
server.js
Normal 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
111
services/auditService.js
Normal 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
34
src/common/config/auth.js
Normal 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
src/common/config/constants.js
Normal file
209
src/common/config/constants.js
Normal 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
src/common/config/database.js
Normal file
49
src/common/config/database.js
Normal 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
|
||||
}
|
||||
};
|
||||
100
src/common/middleware/auth.js
Normal file
100
src/common/middleware/auth.js
Normal 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
|
||||
};
|
||||
76
src/common/middleware/errorHandler.js
Normal file
76
src/common/middleware/errorHandler.js
Normal 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
src/common/middleware/roleCheck.js
Normal file
47
src/common/middleware/roleCheck.js
Normal 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
|
||||
};
|
||||
67
src/common/utils/logger.js
Normal file
67
src/common/utils/logger.js
Normal 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;
|
||||
124
src/database/models/Application.js
Normal file
124
src/database/models/Application.js
Normal 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;
|
||||
};
|
||||
65
src/database/models/AuditLog.js
Normal file
65
src/database/models/AuditLog.js
Normal 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;
|
||||
};
|
||||
87
src/database/models/ConstitutionalChange.js
Normal file
87
src/database/models/ConstitutionalChange.js
Normal 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;
|
||||
};
|
||||
64
src/database/models/Document.js
Normal file
64
src/database/models/Document.js
Normal 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;
|
||||
};
|
||||
75
src/database/models/FinancePayment.js
Normal file
75
src/database/models/FinancePayment.js
Normal 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;
|
||||
};
|
||||
72
src/database/models/FnF.js
Normal file
72
src/database/models/FnF.js
Normal 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;
|
||||
};
|
||||
63
src/database/models/FnFLineItem.js
Normal file
63
src/database/models/FnFLineItem.js
Normal 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;
|
||||
};
|
||||
53
src/database/models/Notification.js
Normal file
53
src/database/models/Notification.js
Normal 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;
|
||||
};
|
||||
104
src/database/models/Outlet.js
Normal file
104
src/database/models/Outlet.js
Normal 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;
|
||||
};
|
||||
44
src/database/models/Region.js
Normal file
44
src/database/models/Region.js
Normal 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;
|
||||
};
|
||||
99
src/database/models/RelocationRequest.js
Normal file
99
src/database/models/RelocationRequest.js
Normal 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;
|
||||
};
|
||||
110
src/database/models/Resignation.js
Normal file
110
src/database/models/Resignation.js
Normal 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;
|
||||
};
|
||||
49
src/database/models/SLAConfiguration.js
Normal file
49
src/database/models/SLAConfiguration.js
Normal 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;
|
||||
};
|
||||
49
src/database/models/SLAEscalationConfig.js
Normal file
49
src/database/models/SLAEscalationConfig.js
Normal 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;
|
||||
};
|
||||
44
src/database/models/SLAReminder.js
Normal file
44
src/database/models/SLAReminder.js
Normal 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;
|
||||
};
|
||||
89
src/database/models/User.js
Normal file
89
src/database/models/User.js
Normal 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;
|
||||
};
|
||||
47
src/database/models/WorkflowStageConfig.js
Normal file
47
src/database/models/WorkflowStageConfig.js
Normal 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;
|
||||
};
|
||||
52
src/database/models/Worknote.js
Normal file
52
src/database/models/Worknote.js
Normal 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;
|
||||
};
|
||||
49
src/database/models/Zone.js
Normal file
49
src/database/models/Zone.js
Normal 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;
|
||||
};
|
||||
55
src/database/models/index.js
Normal file
55
src/database/models/index.js
Normal 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;
|
||||
269
src/modules/auth/auth.controller.js
Normal file
269
src/modules/auth/auth.controller.js
Normal 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'
|
||||
});
|
||||
}
|
||||
};
|
||||
15
src/modules/auth/auth.routes.js
Normal file
15
src/modules/auth/auth.routes.js
Normal 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;
|
||||
84
src/modules/collaboration/collaboration.controller.js
Normal file
84
src/modules/collaboration/collaboration.controller.js
Normal 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' });
|
||||
}
|
||||
};
|
||||
14
src/modules/collaboration/collaboration.routes.js
Normal file
14
src/modules/collaboration/collaboration.routes.js
Normal 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;
|
||||
121
src/modules/master/master.controller.js
Normal file
121
src/modules/master/master.controller.js
Normal 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' });
|
||||
}
|
||||
};
|
||||
28
src/modules/master/master.routes.js
Normal file
28
src/modules/master/master.routes.js
Normal 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;
|
||||
185
src/modules/master/outlet.controller.js
Normal file
185
src/modules/master/outlet.controller.js
Normal 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'
|
||||
});
|
||||
}
|
||||
};
|
||||
16
src/modules/master/outlet.routes.js
Normal file
16
src/modules/master/outlet.routes.js
Normal 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;
|
||||
136
src/modules/onboarding/onboarding.controller.js
Normal file
136
src/modules/onboarding/onboarding.controller.js
Normal 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' });
|
||||
}
|
||||
};
|
||||
15
src/modules/onboarding/onboarding.routes.js
Normal file
15
src/modules/onboarding/onboarding.routes.js
Normal 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;
|
||||
183
src/modules/self-service/constitutional.controller.js
Normal file
183
src/modules/self-service/constitutional.controller.js
Normal 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' });
|
||||
}
|
||||
};
|
||||
237
src/modules/self-service/relocation.controller.js
Normal file
237
src/modules/self-service/relocation.controller.js
Normal 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;
|
||||
}
|
||||
409
src/modules/self-service/resignation.controller.js
Normal file
409
src/modules/self-service/resignation.controller.js
Normal 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);
|
||||
}
|
||||
};
|
||||
13
src/modules/self-service/resignation.routes.js
Normal file
13
src/modules/self-service/resignation.routes.js
Normal 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;
|
||||
26
src/modules/self-service/self-service.routes.js
Normal file
26
src/modules/self-service/self-service.routes.js
Normal 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;
|
||||
99
src/modules/settlement/settlement.controller.js
Normal file
99
src/modules/settlement/settlement.controller.js
Normal 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' });
|
||||
}
|
||||
};
|
||||
17
src/modules/settlement/settlement.routes.js
Normal file
17
src/modules/settlement/settlement.routes.js
Normal 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
67
utils/logger.js
Normal 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;
|
||||
Loading…
Reference in New Issue
Block a user