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