commit 8984a314a755bd3d787129059e62581ff0f112f9 Author: laxmanhalaki Date: Tue Jan 20 19:42:37 2026 +0530 tables added based on the frontend ui and backend is up need to test thouroughly diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..af451cf --- /dev/null +++ b/.gitignore @@ -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 diff --git a/AI-ASSISTANT-INSTRUCTIONS.md b/AI-ASSISTANT-INSTRUCTIONS.md new file mode 100644 index 0000000..23e3339 --- /dev/null +++ b/AI-ASSISTANT-INSTRUCTIONS.md @@ -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!** ๐Ÿš€ \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..937c7dc --- /dev/null +++ b/README.md @@ -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! ๐Ÿš€ diff --git a/back.md b/back.md new file mode 100644 index 0000000..7822d08 --- /dev/null +++ b/back.md @@ -0,0 +1,1270 @@ +# Royal Enfield Dealership Onboarding System - Backend Documentation + +## ๐Ÿ“‹ Overview + +This is a comprehensive Node.js + Express backend for the Royal Enfield Dealership Onboarding System. It handles a multi-stage workflow with 13 different user roles, managing dealer applications, resignations, constitutional changes, relocations, and full F&F settlement processes. + +## ๐Ÿ—๏ธ Architecture + +**Tech Stack:** +- **Runtime:** Node.js (v18+) +- **Framework:** Express.js +- **Database:** PostgreSQL (with Sequelize ORM) +- **Authentication:** JWT (JSON Web Tokens) +- **File Storage:** Multer + Local/Cloud storage +- **Email:** Nodemailer +- **Validation:** express-validator +- **Security:** bcryptjs, helmet, cors + +## ๐Ÿ“ Project Structure + +``` +backend/ +โ”œโ”€โ”€ back.md # This comprehensive documentation file +โ”œโ”€โ”€ package.json # Dependencies and scripts +โ”œโ”€โ”€ .env.example # Environment variables template +โ”œโ”€โ”€ server.js # Main entry point +โ”œโ”€โ”€ config/ +โ”‚ โ”œโ”€โ”€ database.js # Database configuration +โ”‚ โ”œโ”€โ”€ email.js # Email service configuration +โ”‚ โ””โ”€โ”€ constants.js # System constants (roles, statuses, stages) +โ”œโ”€โ”€ models/ +โ”‚ โ”œโ”€โ”€ index.js # Sequelize model loader +โ”‚ โ”œโ”€โ”€ User.js # User model +โ”‚ โ”œโ”€โ”€ Application.js # Dealer application model +โ”‚ โ”œโ”€โ”€ Resignation.js # Resignation request model +โ”‚ โ”œโ”€โ”€ ConstitutionalChange.js # Constitutional change model +โ”‚ โ”œโ”€โ”€ RelocationRequest.js # Relocation request model +โ”‚ โ”œโ”€โ”€ Outlet.js # Dealership/Studio outlet 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 model +โ”‚ โ””โ”€โ”€ Zone.js # Zone model +โ”œโ”€โ”€ 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/ +โ”‚ โ”œโ”€โ”€ 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/ +โ”‚ โ”œโ”€โ”€ 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 +โ”œโ”€โ”€ utils/ +โ”‚ โ”œโ”€โ”€ emailTemplates.js # Email HTML templates +โ”‚ โ”œโ”€โ”€ emailService.js # Email sending utilities +โ”‚ โ”œโ”€โ”€ logger.js # Winston logger +โ”‚ โ””โ”€โ”€ helpers.js # Helper functions +โ””โ”€โ”€ migrations/ # Database migrations + โ””โ”€โ”€ initial-setup.js # Initial database schema +``` + +## ๐Ÿ‘ฅ User Roles & Permissions + +The system supports 13 distinct roles with hierarchical permissions: + +1. **DD (Dealer Development)** - Creates applications, initial review +2. **DD-ZM (DD Zone Manager)** - Zone-level approvals +3. **RBM (Regional Business Manager)** - Regional approvals +4. **ZBH (Zonal Business Head)** - Zonal head approvals +5. **DD Lead** - Organization-wide DD leadership (singular position) +6. **DD Head** - Head of DD department (singular position) +7. **NBH (National Business Head)** - National head (singular position) +8. **DD Admin** - Administrative operations +9. **Legal Admin** - Legal clearance and document verification +10. **Super Admin** - Full system access +11. **DD AM (Area Manager)** - Area-level management +12. **Finance** - Payment tracking and financial approvals +13. **Dealer** - Dealer portal access (resignation, relocation, constitutional changes) + +## ๐Ÿ—„๏ธ Database Schema + +### Key Tables: + +#### 1. **users** +```sql +- id (UUID, PK) +- email (STRING, UNIQUE) +- password (STRING, hashed) +- role (ENUM: DD, DD-ZM, RBM, ZBH, DD Lead, DD Head, NBH, DD Admin, Legal Admin, Super Admin, DD AM, Finance, Dealer) +- name (STRING) +- region (STRING) - East, West, North, South, Central +- zone (STRING) +- status (ENUM: active, inactive) +- createdAt, updatedAt +``` + +#### 2. **applications** +```sql +- id (UUID, PK) +- applicationId (STRING, UNIQUE) - e.g., APP-2026-001 +- applicantName (STRING) +- email (STRING) +- phone (STRING) +- businessType (ENUM: Dealership, Studio) +- proposedLocation (TEXT) +- city, state, pincode +- currentStage (ENUM: DD, DD-ZM, RBM, ZBH, DD Lead, DD Head, NBH, Legal, Finance, Approved, Rejected) +- status (ENUM: Pending, In Review, Approved, Rejected) +- ranking (INTEGER) - DD ranking system +- submittedBy (UUID, FK -> users) +- submittedAt (DATE) +- progressPercentage (INTEGER) +- documents (JSON) - Array of document references +- createdAt, updatedAt +``` + +#### 3. **outlets** +```sql +- id (UUID, PK) +- code (STRING, UNIQUE) - e.g., DL-MH-001, ST-MH-002 +- name (STRING) +- type (ENUM: Dealership, Studio) +- address (TEXT) +- city (STRING) +- state (STRING) +- pincode (STRING) +- latitude (DECIMAL) +- longitude (DECIMAL) +- status (ENUM: Active, Pending Resignation, Closed) +- establishedDate (DATE) +- dealerId (UUID, FK -> users) +- region (STRING) +- zone (STRING) +- createdAt, updatedAt +``` + +#### 4. **resignations** +```sql +- id (UUID, PK) +- resignationId (STRING, UNIQUE) - e.g., RES-001 +- outletId (UUID, FK -> outlets) +- dealerId (UUID, FK -> users) +- resignationType (ENUM: Voluntary, Retirement, Health Issues, Business Closure, Other) +- lastOperationalDateSales (DATE) +- lastOperationalDateServices (DATE) +- reason (TEXT) +- additionalInfo (TEXT) +- currentStage (ENUM: ASM, RBM, ZBH, NBH, DD Admin, Legal, Finance, Completed, Rejected) +- status (STRING) +- progressPercentage (INTEGER) +- submittedOn (DATE) +- documents (JSON) +- createdAt, updatedAt +``` + +#### 5. **constitutional_changes** +```sql +- id (UUID, PK) +- requestId (STRING, UNIQUE) - e.g., CC-001 +- outletId (UUID, FK -> outlets) +- dealerId (UUID, FK -> users) +- changeType (ENUM: Ownership Transfer, Partnership Change, LLP Conversion, Company Formation, Director Change) +- currentStructure (STRING) +- proposedStructure (STRING) +- reason (TEXT) +- effectiveDate (DATE) +- currentStage (ENUM: DD Admin Review, Legal Review, NBH Approval, Finance Clearance, Completed, Rejected) +- status (STRING) +- progressPercentage (INTEGER) +- submittedOn (DATE) +- documents (JSON) +- createdAt, updatedAt +``` + +#### 6. **relocation_requests** +```sql +- id (UUID, PK) +- requestId (STRING, UNIQUE) - e.g., REL-001 +- outletId (UUID, FK -> outlets) +- dealerId (UUID, FK -> users) +- relocationType (ENUM: Within City, Intercity, Interstate) +- currentAddress (TEXT) +- currentLatitude (DECIMAL) +- currentLongitude (DECIMAL) +- proposedAddress (TEXT) +- proposedLatitude (DECIMAL) +- proposedLongitude (DECIMAL) +- proposedCity (STRING) +- proposedState (STRING) +- proposedPincode (STRING) +- distance (DECIMAL) - in kilometers +- reason (TEXT) +- effectiveDate (DATE) +- currentStage (ENUM: DD Admin Review, RBM Review, NBH Approval, Legal Clearance, Completed, Rejected) +- status (STRING) +- progressPercentage (INTEGER) +- submittedOn (DATE) +- documents (JSON) +- createdAt, updatedAt +``` + +#### 7. **worknotes** +```sql +- id (UUID, PK) +- requestId (UUID) - Generic reference to any request +- requestType (ENUM: application, resignation, constitutional, relocation) +- userId (UUID, FK -> users) +- userName (STRING) +- userRole (STRING) +- message (TEXT) +- attachments (JSON) +- timestamp (DATE) +- createdAt, updatedAt +``` + +#### 8. **documents** +```sql +- id (UUID, PK) +- filename (STRING) +- originalName (STRING) +- mimeType (STRING) +- size (INTEGER) +- path (STRING) +- uploadedBy (UUID, FK -> users) +- relatedTo (STRING) - application/resignation/etc +- relatedId (UUID) +- documentType (STRING) - GST, PAN, Aadhaar, etc +- uploadedAt (DATE) +- createdAt, updatedAt +``` + +#### 9. **audit_logs** +```sql +- id (UUID, PK) +- userId (UUID, FK -> users) +- userName (STRING) +- userRole (STRING) +- action (STRING) - CREATED, UPDATED, APPROVED, REJECTED, etc +- entityType (STRING) - application, resignation, etc +- entityId (UUID) +- changes (JSON) - Before/after values +- ipAddress (STRING) +- timestamp (DATE) +- createdAt, updatedAt +``` + +#### 10. **finance_payments** +```sql +- id (UUID, PK) +- applicationId (UUID, FK -> applications) +- outletId (UUID, FK -> outlets) +- dealerId (UUID, FK -> users) +- paymentType (ENUM: Security Deposit, License Fee, Setup Fee, Other) +- amount (DECIMAL) +- dueDate (DATE) +- paidDate (DATE) +- status (ENUM: Pending, Paid, Overdue, Waived) +- transactionId (STRING) +- paymentMode (STRING) +- remarks (TEXT) +- createdAt, updatedAt +``` + +#### 11. **fnf_settlements** +```sql +- id (UUID, PK) +- fnfId (STRING, UNIQUE) - e.g., FNF-001 +- resignationId (UUID, FK -> resignations) +- outletId (UUID, FK -> outlets) +- dealerId (UUID, FK -> users) +- totalDues (DECIMAL) +- securityDepositRefund (DECIMAL) +- otherCharges (DECIMAL) +- netSettlement (DECIMAL) +- status (ENUM: Initiated, DD Clearance, Legal Clearance, Finance Approval, Completed) +- settledDate (DATE) +- progressPercentage (INTEGER) +- createdAt, updatedAt +``` + +#### 12. **regions** +```sql +- id (UUID, PK) +- name (STRING, UNIQUE) - East, West, North, South, Central +- code (STRING) +- headName (STRING) +- headEmail (STRING) +- isActive (BOOLEAN) +- createdAt, updatedAt +``` + +#### 13. **zones** +```sql +- id (UUID, PK) +- name (STRING) +- code (STRING) +- regionId (UUID, FK -> regions) +- managerName (STRING) +- managerEmail (STRING) +- states (JSON) - Array of states covered +- isActive (BOOLEAN) +- createdAt, updatedAt +``` + +## ๐Ÿ”Œ API Endpoints + +### Authentication + +#### `POST /api/auth/login` +```json +Request: +{ + "email": "user@example.com", + "password": "password123" +} + +Response: +{ + "success": true, + "token": "jwt_token_here", + "user": { + "id": "uuid", + "email": "user@example.com", + "name": "John Doe", + "role": "DD" + } +} +``` + +#### `POST /api/auth/logout` +```json +Headers: { Authorization: "Bearer jwt_token" } + +Response: +{ + "success": true, + "message": "Logged out successfully" +} +``` + +#### `GET /api/auth/me` +```json +Headers: { Authorization: "Bearer jwt_token" } + +Response: +{ + "success": true, + "user": { + "id": "uuid", + "email": "user@example.com", + "name": "John Doe", + "role": "DD", + "region": "West", + "zone": "Mumbai" + } +} +``` + +### Outlets + +#### `GET /api/outlets/my-outlets` +Get all outlets for logged-in dealer +```json +Headers: { Authorization: "Bearer dealer_token" } + +Response: +{ + "success": true, + "outlets": [ + { + "id": "uuid", + "code": "DL-MH-001", + "name": "Royal Enfield Mumbai", + "type": "Dealership", + "address": "Bandra West, Mumbai", + "status": "Active", + "hasActiveResignation": false + } + ] +} +``` + +### Resignations + +#### `POST /api/resignations/create` +Create resignation request (Dealer only) +```json +Headers: { Authorization: "Bearer dealer_token" } + +Request: +{ + "outletId": "uuid", + "resignationType": "Voluntary", + "lastOperationalDateSales": "2026-02-28", + "lastOperationalDateServices": "2026-02-28", + "reason": "Personal health issues", + "additionalInfo": "Optional details" +} + +Response: +{ + "success": true, + "resignationId": "RES-001", + "message": "Resignation request submitted successfully" +} +``` + +#### `GET /api/resignations/list` +Get resignations (role-based filtering) +```json +Headers: { Authorization: "Bearer jwt_token" } + +Response: +{ + "success": true, + "resignations": [ + { + "resignationId": "RES-001", + "outlet": { "code": "DL-MH-001", "name": "..." }, + "resignationType": "Voluntary", + "currentStage": "ASM Review", + "status": "Pending", + "progressPercentage": 15 + } + ] +} +``` + +#### `GET /api/resignations/:id` +Get resignation details +```json +Headers: { Authorization: "Bearer jwt_token" } + +Response: +{ + "success": true, + "resignation": { + "resignationId": "RES-001", + "outlet": {...}, + "resignationType": "Voluntary", + "reason": "...", + "timeline": [...], + "worknotes": [...] + } +} +``` + +#### `POST /api/resignations/:id/approve` +Approve resignation at current stage +```json +Headers: { Authorization: "Bearer jwt_token" } + +Request: +{ + "remarks": "Approved by ASM" +} + +Response: +{ + "success": true, + "message": "Resignation approved and moved to next stage" +} +``` + +#### `POST /api/resignations/:id/reject` +Reject resignation +```json +Headers: { Authorization: "Bearer jwt_token" } + +Request: +{ + "reason": "Incomplete documentation" +} + +Response: +{ + "success": true, + "message": "Resignation rejected" +} +``` + +### Constitutional Changes + +#### `POST /api/constitutional/create` +Create constitutional change request (Dealer only) +```json +Headers: { Authorization: "Bearer dealer_token" } + +Request: +{ + "outletId": "uuid", + "changeType": "Partnership Change", + "currentStructure": "Sole Proprietorship", + "proposedStructure": "Partnership Firm", + "reason": "Business expansion", + "effectiveDate": "2026-03-01" +} + +Response: +{ + "success": true, + "requestId": "CC-001" +} +``` + +#### `GET /api/constitutional/list` +Get constitutional change requests +```json +Headers: { Authorization: "Bearer jwt_token" } + +Response: +{ + "success": true, + "requests": [...] +} +``` + +### Relocation Requests + +#### `POST /api/relocations/create` +Create relocation request (Dealer only) +```json +Headers: { Authorization: "Bearer dealer_token" } + +Request: +{ + "outletId": "uuid", + "relocationType": "Within City", + "proposedAddress": "New location address", + "proposedLatitude": 19.0760, + "proposedLongitude": 72.8777, + "proposedCity": "Mumbai", + "proposedState": "Maharashtra", + "proposedPincode": "400050", + "reason": "Better footfall location", + "effectiveDate": "2026-04-01" +} + +Response: +{ + "success": true, + "requestId": "REL-001" +} +``` + +#### `GET /api/relocations/calculate-distance` +Calculate distance between two locations +```json +Headers: { Authorization: "Bearer jwt_token" } + +Query: ?fromLat=19.0760&fromLng=72.8777&toLat=19.1196&toLng=72.9046 + +Response: +{ + "success": true, + "distance": 5.2, + "unit": "km" +} +``` + +### Worknotes + +#### `POST /api/worknotes/create` +Add worknote to any request +```json +Headers: { Authorization: "Bearer jwt_token" } + +Request: +{ + "requestId": "uuid", + "requestType": "resignation", + "message": "Please provide updated documents" +} + +Response: +{ + "success": true, + "worknote": {...} +} +``` + +#### `GET /api/worknotes/:requestId/:requestType` +Get all worknotes for a request +```json +Headers: { Authorization: "Bearer jwt_token" } + +Response: +{ + "success": true, + "worknotes": [ + { + "id": "uuid", + "userName": "John Doe", + "userRole": "DD Admin", + "message": "...", + "timestamp": "2026-01-13T10:30:00Z" + } + ] +} +``` + +### Applications + +#### `POST /api/applications/create` +Create new dealer application (public endpoint) +```json +Request: +{ + "applicantName": "Rajesh Kumar", + "email": "rajesh@example.com", + "phone": "+91-9876543210", + "businessType": "Dealership", + "proposedLocation": "Full address", + "city": "Mumbai", + "state": "Maharashtra", + "pincode": "400001" +} + +Response: +{ + "success": true, + "applicationId": "APP-2026-001" +} +``` + +#### `GET /api/applications/list` +Get applications (role-based filtering) +```json +Headers: { Authorization: "Bearer jwt_token" } +Query: ?status=Pending®ion=West&page=1&limit=10 + +Response: +{ + "success": true, + "applications": [...], + "total": 50, + "page": 1, + "pages": 5 +} +``` + +#### `POST /api/applications/:id/assign-ranking` +DD assigns ranking (1-5) +```json +Headers: { Authorization: "Bearer dd_token" } + +Request: +{ + "ranking": 4 +} + +Response: +{ + "success": true, + "message": "Ranking assigned" +} +``` + +#### `POST /api/applications/:id/move-stage` +Move application to next stage +```json +Headers: { Authorization: "Bearer jwt_token" } + +Request: +{ + "action": "approve", // or "reject" + "remarks": "All documents verified" +} + +Response: +{ + "success": true, + "nextStage": "DD-ZM" +} +``` + +### Finance + +#### `GET /api/finance/onboarding-payments` +Get all pending onboarding payments +```json +Headers: { Authorization: "Bearer finance_token" } + +Response: +{ + "success": true, + "payments": [...] +} +``` + +#### `POST /api/finance/payment/:id/mark-paid` +Mark payment as received +```json +Headers: { Authorization: "Bearer finance_token" } + +Request: +{ + "transactionId": "TXN123456", + "paidDate": "2026-01-13", + "paymentMode": "NEFT" +} + +Response: +{ + "success": true, + "message": "Payment marked as paid" +} +``` + +#### `GET /api/finance/fnf-list` +Get F&F settlement requests +```json +Headers: { Authorization: "Bearer finance_token" } + +Response: +{ + "success": true, + "fnfRequests": [...] +} +``` + +### Dashboard + +#### `GET /api/dashboard/stats` +Get dashboard statistics (role-based) +```json +Headers: { Authorization: "Bearer jwt_token" } + +Response: +{ + "success": true, + "stats": { + "totalApplications": 150, + "pendingApplications": 45, + "approvedApplications": 95, + "rejectedApplications": 10, + "pendingResignations": 5, + "pendingRelocations": 3 + } +} +``` + +### Master Configuration + +#### `GET /api/master/regions` +Get all regions +```json +Headers: { Authorization: "Bearer jwt_token" } + +Response: +{ + "success": true, + "regions": [ + { + "id": "uuid", + "name": "East", + "code": "EAST", + "zones": [...] + } + ] +} +``` + +#### `POST /api/master/regions/create` +Create region (Super Admin only) +```json +Headers: { Authorization: "Bearer admin_token" } + +Request: +{ + "name": "East", + "code": "EAST", + "headName": "Amit Kumar", + "headEmail": "amit@example.com" +} + +Response: +{ + "success": true, + "region": {...} +} +``` + +### File Upload + +#### `POST /api/upload/document` +Upload document +```json +Headers: { + Authorization: "Bearer jwt_token", + Content-Type: "multipart/form-data" +} + +FormData: +- file: (binary) +- relatedTo: "resignation" +- relatedId: "uuid" +- documentType: "GST Certificate" + +Response: +{ + "success": true, + "document": { + "id": "uuid", + "filename": "unique-filename.pdf", + "originalName": "gst-certificate.pdf", + "url": "/uploads/documents/unique-filename.pdf" + } +} +``` + +## ๐Ÿ” Authentication & Authorization + +### JWT Authentication Flow: + +1. **Login:** User sends credentials โ†’ Server validates โ†’ Returns JWT token +2. **Protected Routes:** Client sends token in Authorization header +3. **Token Verification:** Middleware validates token before processing request +4. **Token Expiry:** 24 hours (configurable) + +### Role-Based Access Control (RBAC): + +Middleware checks user role against route permissions: + +```javascript +// Example: Only DD Lead can access opportunity requests +router.get('/opportunity-requests', + auth, + roleCheck(['DD Lead']), + getOpportunityRequests +); + +// Example: Multiple roles can approve resignations +router.post('/resignations/:id/approve', + auth, + roleCheck(['DD Admin', 'RBM', 'ZBH', 'NBH', 'Legal Admin']), + approveResignation +); +``` + +### Permission Matrix: + +| Feature | DD | DD-ZM | RBM | ZBH | DD Lead | DD Head | NBH | DD Admin | Legal | Finance | Dealer | +|---------|----|----|-----|-----|---------|---------|-----|----------|-------|---------|--------| +| Create Application | โœ… | โŒ | โŒ | โŒ | โœ… | โœ… | โŒ | โŒ | โŒ | โŒ | โŒ | +| View All Applications | โœ… | โœ… | โœ… | โœ… | โœ… | โœ… | โœ… | โœ… | โœ… | โœ… | โŒ | +| Approve Application Stage | โœ… | โœ… | โœ… | โœ… | โœ… | โœ… | โœ… | โŒ | โœ… | โœ… | โŒ | +| Create Resignation | โŒ | โŒ | โŒ | โŒ | โŒ | โŒ | โŒ | โŒ | โŒ | โŒ | โœ… | +| Approve Resignation | โŒ | โŒ | โœ… | โœ… | โŒ | โŒ | โœ… | โœ… | โœ… | โœ… | โŒ | +| Constitutional Change | โŒ | โŒ | โŒ | โŒ | โŒ | โŒ | โœ… | โœ… | โœ… | โœ… | โœ… | +| Relocation Request | โŒ | โŒ | โœ… | โŒ | โŒ | โŒ | โœ… | โœ… | โœ… | โŒ | โœ… | +| Master Configuration | โŒ | โŒ | โŒ | โŒ | โœ… | โŒ | โŒ | โœ… | โŒ | โŒ | โŒ | + +## ๐Ÿ“ง Email Notifications + +### Email Triggers: + +1. **Application Submitted** โ†’ Notify assigned DD +2. **Application Approved/Rejected** โ†’ Notify applicant +3. **Stage Changed** โ†’ Notify next approver +4. **Deadline Approaching** โ†’ Reminder email (3 days, 1 day before) +5. **Resignation Submitted** โ†’ Notify DD Admin +6. **Constitutional Change** โ†’ Notify Legal team +7. **Relocation Request** โ†’ Notify RBM +8. **Payment Due** โ†’ Notify dealer +9. **F&F Initiated** โ†’ Notify Finance team + +### Email Templates: + +Located in `utils/emailTemplates.js`: +- Application confirmation +- Approval notification +- Rejection notification +- Stage progression +- Deadline reminder +- Password reset + +## ๐Ÿš€ Setup Instructions + +### 1. Prerequisites + +Install the following on your system: +- Node.js v18+ (https://nodejs.org) +- PostgreSQL 14+ (https://www.postgresql.org) +- Git (optional) + +### 2. Install Dependencies + +```bash +cd backend +npm install +``` + +### 3. Database Setup + +Create PostgreSQL database: +```sql +CREATE DATABASE royal_enfield_onboarding; +CREATE USER re_admin WITH PASSWORD 'your_password'; +GRANT ALL PRIVILEGES ON DATABASE royal_enfield_onboarding TO re_admin; +``` + +### 4. Environment Configuration + +Copy `.env.example` to `.env` and configure: + +```env +# Server +NODE_ENV=development +PORT=5000 + +# Database +DB_HOST=localhost +DB_PORT=5432 +DB_NAME=royal_enfield_onboarding +DB_USER=re_admin +DB_PASSWORD=your_password + +# JWT +JWT_SECRET=your_super_secret_jwt_key_here_make_it_long_and_random +JWT_EXPIRES_IN=24h + +# Email (Gmail example) +EMAIL_HOST=smtp.gmail.com +EMAIL_PORT=587 +EMAIL_USER=your-email@gmail.com +EMAIL_PASSWORD=your-app-specific-password +EMAIL_FROM=Royal Enfield + +# File Upload +UPLOAD_PATH=./uploads +MAX_FILE_SIZE=10485760 + +# Frontend URL (for CORS) +FRONTEND_URL=http://localhost:5173 + +# Admin Default Password (for initial setup) +ADMIN_DEFAULT_PASSWORD=Admin@123 +``` + +### 5. Run Migrations + +```bash +npm run migrate +``` + +This creates all tables and seeds initial data: +- Super Admin user +- 5 Regions (East, West, North, South, Central) +- Sample zones +- Sample users for each role + +### 6. Start Server + +**Development:** +```bash +npm run dev +``` + +**Production:** +```bash +npm start +``` + +Server runs on: `http://localhost:5000` + +### 7. Test API + +Use Postman/Thunder Client: + +```bash +# Login as Super Admin +POST http://localhost:5000/api/auth/login +Body: { + "email": "admin@royalenfield.com", + "password": "Admin@123" +} + +# Get dashboard stats +GET http://localhost:5000/api/dashboard/stats +Headers: Authorization: Bearer +``` + +## ๐Ÿ”— Frontend Integration + +### Update Frontend API Calls + +In your frontend, update API base URL: + +**Create `/src/lib/api.ts`:** +```typescript +const API_BASE_URL = process.env.NODE_ENV === 'production' + ? 'https://your-backend-domain.com/api' + : 'http://localhost:5000/api'; + +export const api = { + async login(email: string, password: string) { + const response = await fetch(`${API_BASE_URL}/auth/login`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ email, password }) + }); + return response.json(); + }, + + async getMyOutlets(token: string) { + const response = await fetch(`${API_BASE_URL}/outlets/my-outlets`, { + headers: { 'Authorization': `Bearer ${token}` } + }); + return response.json(); + }, + + async createResignation(token: string, data: any) { + const response = await fetch(`${API_BASE_URL}/resignations/create`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${token}` + }, + body: JSON.stringify(data) + }); + return response.json(); + } +}; +``` + +### Replace Mock Data + +**Before (Mock):** +```typescript +const mockOutlets = [...]; // Hardcoded data +``` + +**After (Real API):** +```typescript +useEffect(() => { + const fetchOutlets = async () => { + const token = localStorage.getItem('token'); + const result = await api.getMyOutlets(token); + setOutlets(result.outlets); + }; + fetchOutlets(); +}, []); +``` + +## ๐Ÿ“Š Workflow State Machines + +### Application Workflow: +``` +Initial โ†’ DD Review โ†’ DD-ZM Review โ†’ RBM Review โ†’ ZBH Review +โ†’ DD Lead Review โ†’ DD Head Review โ†’ NBH Approval โ†’ Legal Clearance +โ†’ Finance Payment โ†’ Approved โœ… +``` + +### Resignation Workflow: +``` +Submitted โ†’ ASM Review โ†’ RBM Review โ†’ ZBH Review โ†’ NBH Approval +โ†’ DD Admin Clearance โ†’ Legal Clearance โ†’ F&F Initiation +โ†’ Finance Settlement โ†’ Completed โœ… +``` + +### Constitutional Change Workflow: +``` +Submitted โ†’ DD Admin Review โ†’ Legal Verification โ†’ NBH Approval +โ†’ Finance Clearance โ†’ Completed โœ… +``` + +### Relocation Workflow: +``` +Submitted โ†’ DD Admin Review โ†’ RBM Assessment โ†’ NBH Approval +โ†’ Legal Clearance โ†’ Completed โœ… +``` + +## ๐Ÿ›ก๏ธ Security Features + +1. **Password Hashing:** bcryptjs with salt rounds +2. **JWT Tokens:** Secure token-based authentication +3. **CORS:** Configured for frontend domain only +4. **Helmet:** Security headers +5. **Rate Limiting:** Prevent brute force attacks +6. **Input Validation:** express-validator on all inputs +7. **SQL Injection Prevention:** Sequelize ORM parameterized queries +8. **File Upload Validation:** Type and size restrictions +9. **Audit Logging:** All actions logged with user/timestamp + +## ๐Ÿ“ Logging + +Winston logger with multiple transports: +- **Console:** Development logging +- **File:** `logs/error.log` - Error logs +- **File:** `logs/combined.log` - All logs + +## ๐Ÿงช Testing + +```bash +# Run tests +npm test + +# Run with coverage +npm run test:coverage +``` + +## ๐Ÿšข Deployment + +### Option 1: Railway (Recommended) + +1. Create account on railway.app +2. Connect GitHub repository +3. Add PostgreSQL plugin +4. Set environment variables +5. Deploy automatically + +### Option 2: Render + +1. Create account on render.com +2. Create Web Service +3. Connect repository +4. Add PostgreSQL database +5. Set environment variables +6. Deploy + +### Option 3: AWS/DigitalOcean + +1. Set up EC2/Droplet +2. Install Node.js, PostgreSQL +3. Clone repository +4. Configure environment +5. Use PM2 for process management +6. Set up Nginx reverse proxy + +### Environment Variables for Production: + +```env +NODE_ENV=production +DB_HOST= +DB_NAME= +DB_USER= +DB_PASSWORD= +JWT_SECRET= +EMAIL_HOST= +EMAIL_USER= +EMAIL_PASSWORD= +FRONTEND_URL=https://your-frontend-domain.com +``` + +## ๐Ÿ“ฆ Dependencies + +**Core:** +- express: Web framework +- sequelize: ORM +- pg, pg-hstore: PostgreSQL driver +- jsonwebtoken: JWT authentication +- bcryptjs: Password hashing + +**Middleware:** +- cors: Cross-origin resource sharing +- helmet: Security headers +- express-validator: Input validation +- multer: File uploads + +**Utilities:** +- nodemailer: Email sending +- winston: Logging +- dotenv: Environment variables +- uuid: Unique IDs + +**Dev Dependencies:** +- nodemon: Auto-restart on changes +- jest: Testing framework + +## ๐Ÿ”ง Maintenance + +### Database Backup: +```bash +pg_dump -U re_admin royal_enfield_onboarding > backup.sql +``` + +### Restore Database: +```bash +psql -U re_admin royal_enfield_onboarding < backup.sql +``` + +### Clear Logs: +```bash +npm run clear-logs +``` + +## ๐Ÿ“ž Support & Troubleshooting + +### Common Issues: + +**1. Database Connection Error:** +- Check PostgreSQL is running: `sudo service postgresql status` +- Verify credentials in `.env` +- Check database exists: `psql -l` + +**2. Port Already in Use:** +- Change PORT in `.env` +- Kill process: `lsof -ti:5000 | xargs kill` + +**3. JWT Token Invalid:** +- Check JWT_SECRET is same across restarts +- Verify token expiry time +- Clear old tokens from frontend + +**4. File Upload Failing:** +- Check UPLOAD_PATH directory exists and is writable +- Verify MAX_FILE_SIZE setting +- Check disk space + +**5. Email Not Sending:** +- Verify SMTP credentials +- Check firewall/port 587 access +- Enable "Less secure apps" for Gmail or use App Password + +## ๐ŸŽฏ Next Steps for AI Assistant (Cursor/Windsurf/etc) + +When you provide this backend to your AI IDE, it will understand: + +1. โœ… Complete architecture and file structure +2. โœ… Database schema and relationships +3. โœ… All API endpoints and their contracts +4. โœ… Authentication and authorization flow +5. โœ… Environment setup requirements +6. โœ… Deployment options +7. โœ… Integration points with frontend + +**The AI can then:** +- Help you set up the database +- Debug any issues +- Add new features +- Optimize queries +- Handle deployment +- Write tests +- Generate documentation + +## ๐Ÿ“„ License + +Proprietary - Royal Enfield Dealership Onboarding System + +--- + +**Created:** January 2026 +**Version:** 1.0.0 +**Last Updated:** January 13, 2026 + +For questions or support, refer to this documentation first, then consult with your development team. diff --git a/config/auth.js b/config/auth.js new file mode 100644 index 0000000..c402910 --- /dev/null +++ b/config/auth.js @@ -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 +}; diff --git a/config/constants.js b/config/constants.js new file mode 100644 index 0000000..a8227b1 --- /dev/null +++ b/config/constants.js @@ -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 +}; diff --git a/config/database.js b/config/database.js new file mode 100644 index 0000000..8b9fd3b --- /dev/null +++ b/config/database.js @@ -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 + } +}; diff --git a/config/email.js b/config/email.js new file mode 100644 index 0000000..4f241ce --- /dev/null +++ b/config/email.js @@ -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 ' +}; diff --git a/controllers/applicationController.js b/controllers/applicationController.js new file mode 100644 index 0000000..56e8567 --- /dev/null +++ b/controllers/applicationController.js @@ -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' }); + } +}; diff --git a/controllers/authController.js b/controllers/authController.js new file mode 100644 index 0000000..e8f3a19 --- /dev/null +++ b/controllers/authController.js @@ -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' + }); + } +}; diff --git a/controllers/constitutionalController.js b/controllers/constitutionalController.js new file mode 100644 index 0000000..2c15e7b --- /dev/null +++ b/controllers/constitutionalController.js @@ -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' }); + } +}; diff --git a/controllers/financeController.js b/controllers/financeController.js new file mode 100644 index 0000000..ed260e8 --- /dev/null +++ b/controllers/financeController.js @@ -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' }); + } +}; diff --git a/controllers/masterController.js b/controllers/masterController.js new file mode 100644 index 0000000..5e85814 --- /dev/null +++ b/controllers/masterController.js @@ -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' }); + } +}; diff --git a/controllers/outletController.js b/controllers/outletController.js new file mode 100644 index 0000000..88f80cd --- /dev/null +++ b/controllers/outletController.js @@ -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' + }); + } +}; diff --git a/controllers/relocationController.js b/controllers/relocationController.js new file mode 100644 index 0000000..ee23194 --- /dev/null +++ b/controllers/relocationController.js @@ -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; +} diff --git a/controllers/resignationController.js b/controllers/resignationController.js new file mode 100644 index 0000000..307c88f --- /dev/null +++ b/controllers/resignationController.js @@ -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; diff --git a/controllers/worknoteController.js b/controllers/worknoteController.js new file mode 100644 index 0000000..9ce8eb3 --- /dev/null +++ b/controllers/worknoteController.js @@ -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' }); + } +}; diff --git a/docs/Backend_Alignment_Report_v1.4.md b/docs/Backend_Alignment_Report_v1.4.md new file mode 100644 index 0000000..a9d5d40 --- /dev/null +++ b/docs/Backend_Alignment_Report_v1.4.md @@ -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. diff --git a/docs/Comparison_Summary_v1.0_vs_v1.4.md b/docs/Comparison_Summary_v1.0_vs_v1.4.md new file mode 100644 index 0000000..f837bdd --- /dev/null +++ b/docs/Comparison_Summary_v1.0_vs_v1.4.md @@ -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* diff --git a/docs/Re_New_Dealer_Onboard_TWO.md b/docs/Re_New_Dealer_Onboard_TWO.md new file mode 100644 index 0000000..e7f553d --- /dev/null +++ b/docs/Re_New_Dealer_Onboard_TWO.md @@ -0,0 +1,7220 @@ +# RE Onboarding & Offboarding System + +# Requirements + +``` +System Requirements Specifications +``` +## 16 - Oct- 2025 + +## Version 1. 4 + + +## Contents + + +- Change Log + - 1.1 Change Log โ€“ Version 2.0 + - 1.2 Change Log โ€“ Dealer Self-Service Enablement +- 1 System Overview & Problem Statement +- 2 Intended Audience + - 2.1 Business & Functional Users + - 2.2 External & Integrated Stakeholders +- 3 Definitions and Acronyms +- 4 HiFi Wireframes & Flow of Application + - 4.1 Dealer onboarding - Process Flow Overview + - 4.2 Dealer Resignation โ€“ Process Flow Overview + - 4.3 Dealer Termination โ€“ Process Flow Overview + - 4.4 Dealer Full & Final (F&F) Settlement โ€“ Process Flow + - 4.5 Finance Team โ€“ Process Flow +- 5 System Features & Requirements +- 6 Dealer onboarding + - 6.1 Dealership Application Form + - 6.2 SSO Login + - 6.3 Dashboard + - 6.4 Opportunity & Non Opportunity + - 6.5 Questionnaire Response + - 6.6 Shortlisting Process + - 6.7 Shortlisted Applicants + - 6.8 Application Detail View + - 6.9 Interview Scheduling & Coordination + - 6.10 Interview Evaluation & Feedback Management + - 6.11 Interview Feedback & Evaluation Summary + - 6.12 Application Approval & Rejection Workflow + - 6.13 Work Notes & Internal Communication Trail + - 6.14 System Notifications & Alerts + - 6.15 FDD (Financial Due Diligence) & Finance Module + - 6.16 LOI Approval & Issuance + - 6.17 Dealer Code Generation, Architectural Work & Statutory Documentation............ + - 6.18 LOA Issuance, Essential Operating Requirements & Inauguration + - 6.19 Essential Operating Requirements (EOR) Checklist + - 6.20 Progress Tracker....................................................................................................... + - 6.21 Central Document Repository + - 6.22 Audit Trail & Activity Log.......................................................................................... +- 7 Dealer Resignation + - 7.1 Dealer Resignation Request (Initiation) + - 7.2 Resignation Management Dashboard + - 7.3 Resignation Details & Review + - 7.4 Resignation Request Review & Action Management + - 7.5 Resignation Progress Tracker + - 7.6 Documents & Audit Trail +- 8 Termination + - 8.1 Create Termination Request + - 8.2 Termination Ticket overview + - 8.3 Termination Approval & Review Process + - 8.4 Termination Progress Timeline +- 9 Admin Section + - 9.1 Master Configuration โ€“ Organization + - 9.2 Zone, Region & Area Configuration + - 9.3 Roles & permissions + - 9.4 SLA Configuration & Escalation Management + - 9.5 Email & Letter Templates Management + - 9.6 Opportunity Management (Geography & Window Setup) +- 10 F&F Case + - 10.1 F&F Settlement Progress Timeline + - 10.2 Department Responses +- 11 Finance Dashboard + - 11.1 Finance Dashboard Page + - 11.2 F&F Settlement Module +- 12 Dealer Persona + - 12.1 Dealer Resignation + - 12.2 Dealer Constitutional Change Management +- 13 Non-Functional Requirements +- 14 Technology Matrix +- 15 Infra requirements & System Hygiene +- 16 Not in scope + + +## Change Log + +### 1.1 Change Log โ€“ Version 2.0 + +**Module:** Dealer Onboarding & Offboarding System +**Change Type:** Clarifications, Role Alignment & Access Control Enhancements +**Scope Enhancement :** Dealer Role and Access control +**Change demarcation** : Highlighted in Yellow +**Changes suggested by** : Ashok & Tariq +**Changed performed by** : Rohit Mandiwal +**Changes done on** : 31 - Dec- 2025 + +**1.1.1 Notification Channel Enhancement** + +- Added **WhatsApp as a supported notification channel** for reminders and workflow + communications (e.g., questionnaire completion and status updates), while restricting + sensitive document sharing to email only. + +**1.1.2 LOI Governance & Communication Clarifications** + +- Clarified that the **Finance team is not the decision-making authority** for LOI issuance + and is responsible only for financial validation. +- Confirmed that **LOI documents are shared exclusively via official email** and not through + WhatsApp. +- Clarified that **LOA issuance is a parallel statutory activity** and is **not dependent on** + **infrastructure readiness**. + +**1.1.3 Dealer Code Creation Control** + +- Clarified that **Dealer Codes (SAP Master) are created only upon explicit trigger by the** + **DD Admin** , and not through automatic system generation. + +**1.1.4 LOA & EOR Sequencing Correction** + +- Corrected the workflow sequence to ensure that **LOA is issued prior to initiating the** + **EOR checklist** , with EOR serving as the final readiness validation before go-live. + +**1.1.5 Dealer Resignation Access & Workflow Enhancements** + +- Enabled **dealer portal access** for initiating resignation requests and uploading required + information. + + +- Clarified that the **Legal team issues the Resignation Acceptance Letter** in all cases. +- Expanded review authority to allow **ZBH, DD Lead, DD Head, and NBH** to **Send Back or** + **Revoke resignation requests** , with communication routed through **Work Notes**. +- Confirmed that **Full & Final (F&F) settlement is triggered strictly on the Last Working** + **Day (LWD)** and not based on approval date. + +**1.1.6 Termination Workflow Governance Updates** + +- Clarified that **CEO is the final approving authority** for dealer termination cases. +- Included **CCO and CEO** as approval authorities with **Approve / Hold / Reject** options. +- Confirmed that the **Legal team issues termination letters only after CEO approval**. +- Removed **dealer portal access** from termination workflows. +- Extended **Send Back / Revoke** authority to **ZBH and DD Lead** for termination reviews. +- Aligned **F&F trigger for termination** to occur strictly on the **Last Working Day (LWD)**. + +**1.1.7 Role & Persona Alignment** + +- Added **NBH** to the personas section. +- Added **RBM** to applicable review and approval tables. +- Clarified that **DD ASM is responsible for interview scheduling and coordination** , with + no Admin involvement. + +**1.1.8 Access Control & Visibility Refinements** + +- Defined **view-only access** for DD ASM, DD ZM, and RBM at relevant workflow stages. +- Granted **approval visibility** to DD Lead where applicable. +- Enabled **DD ASM and DD ZM** to upload site readiness and LOA-related documents, + with **DD Lead, RBM, and ZBH** having view access. +- Limited applicant and dealer portal access to **stage-specific and context-specific** + **scenarios only**. +- Confirmed that **dealer portal access is revoked after resignation or termination**. + +**1.1.9 Terminology & Documentation Corrections** + +- Clarified **KT Matrix as Kepner Tregoe Matrix** for consistency and correctness. + +**1.1.10 Super Admin Role Introduction** + +- Introduced a **Super Admin (Master Role)** with end-to-end access and workflow control + across modules. +- Defined segregation of duties by splitting Super Admin into **two DD Admin roles** with + clearly scoped responsibilities. + + +### 1.2 Change Log โ€“ Dealer Self-Service Enablement + +**Version:** v2. +**Section Impacted:** Section 12 โ€“ Dealer Portal (12.1 onwards) +**Module:** Dealer Onboarding & Offboarding System +**Change Type:** Dealer Feature Enablement (Section 12 onwards) +**Scope Enhancement :** Dealer Role and Access control +**Change demarcation** : Highlighted in Yellow +**Changes suggested by** : Ashok & Tariq +**Changed performed by** : Rohit Mandiwal +**Changes done on** : 5 - Jan- 2026 + +**1.2.1 Introduction of Dealer Portal** + +- Introduced a **Dealer Portal capability** enabling onboarded dealers to initiate and track + post-onboarding lifecycle requests through the portal. +- Dealer actions are governed by **role-based access controls** , approval hierarchies, and + audit mechanisms. + +**1.2.2 Dealer Resignation Enablement** + +- Enabled **dealer-initiated resignation requests** at outlet level via the portal. +- Added structured resignation submission with: + o Last Operational Date (Sales & Services) + o Reason for resignation + o Mandatory document readiness guidance +- Enabled **dealer withdrawal option** for resignation requests **only until the case is** + **pending with NBH**. +- Clarified that **Legal team issues the Resignation Acceptance Letter** post approvals. +- Ensured **F&F settlement is triggered based on Last Working Day (LWD)** and not + approval date. +- Restricted dealer portal access **post resignation closure**. + +**1.2.3 Dealer Relocation Request Enablement** + +- Enabled dealers to **initiate and track relocation requests** through a guided workflow. +- Added support for: + o Manual or map-based location entry + o Distance calculation from existing location + o Property type selection and expected relocation date + + +- Introduced **document-driven relocation validation** , including statutory, legal, property, + and infrastructure documents. +- Implemented **multi-level approval workflow** with Work Notesโ€“based communication + and audit trail. +- Ensured dealer has **view and upload access only** , with approvals retained by RE + stakeholders. + +**1.2.4 Dealer Constitutional Change Enablement** + +- Enabled dealers to **initiate constitutional change requests** post onboarding. +- Supported all approved constitution change scenarios: + o Proprietorship, Partnership, LLP, and Private Limited permutations +- Implemented **dynamic document requirement determination** based on target + constitution. +- Explicitly confirmed **no OCR-based document validation** ; all validations are manual and + role-driven. +- Ensured statutory compliance via Legal review before master data updates. + +**1.2.5 Post-Exit Access Control** + +- Enforced system rule to **revoke dealer portal access** once resignation or termination is + completed. + + +## 1 System Overview & Problem Statement + +**1.1.1 System Overview** + +The **Dealer Onboarding and Offboarding System** for **Royal Enfield (RE)** is designed to **digitize, +standardize, and streamline** the complete dealer lifecycle โ€” from **application and +evaluation** to **approval, resignation, termination, and full-and-final (F&F) settlement**. + +At present, the process operates through **manual coordination** , involving **emails, spreadsheets, +and physical documentation** , which makes it difficult to maintain visibility, accountability, and +consistency across teams. + +The proposed solution introduces a **centralized digital platform** that brings all stakeholders onto +a single workflow. It ensures that every stage โ€” **onboarding, operational approvals, financial +diligence, legal validation, and final closure** โ€” follows a **structured and traceable process**. + +The system integrates seamlessly with existing RE applications such as **SSO** , **SAP** , and **Finance +modules** , providing **role-based access** , **real-time tracking** , and **secure document management**. +It also offers **automated workflows** , **configurable approval hierarchies** , and **AI-assisted decision +support** to improve efficiency and reduce turnaround time. + +By moving to a digital workflow, Royal Enfield will achieve higher levels of **process +efficiency** , **data accuracy** , and **transparency** , ensuring faster decision-making and stronger +control over the dealer network lifecycle. + +## 2 Intended Audience + +This document is intended for all stakeholders involved in the **design, implementation, approval, +and operational use** of the **Dealer Onboarding and Offboarding System** at **Royal Enfield (RE)**. + +The following user personas and roles are part of the system: + +### 2.1 Business & Functional Users + +**2.1.1 Dealer Development (DD) Team** + +- **Super Admin (Master Role):** + The **Super Admin has unrestricted access** across all modules and workflows, with + authority to **configure, override, and influence workflow behavior** at every level. + + +``` +The Super Admin role is segregated into two DD Admin roles , each with clearly defined +scopes to ensure segregation of duties and governance control. +``` +- **DD-Admin:** System administrator responsible for user setup, role mapping, hierarchy + configuration, and workflow management. +- **DD-AM (Area Manager):** Reviews and manages applications within assigned regions; + performs preliminary screening. +- **DD-ZM (Zonal Manager):** Conducts the first level of dealer evaluation along with RBM; + prepares presentation decks for final interviews. +- **DD-Lead:** Reviews zonal evaluations, validates recommendations, and forwards + shortlisted applicants for senior-level approval. +- **DD-Head: DD Head** is engaged in the **final review and approval** of shortlisted dealer + applications before the **NBH interview** , and later **oversees final verification and LOI** + **issuance** after all evaluations are complete. + +**2.1.2 Regional Sales & Business Team** + +- **RBM (Regional Business Manager):** Participates in early-stage evaluations, provides + ground-level business insights, and recommends suitable candidates. +- **ZBH (Zonal Business Head):** Conducts the second-level review along with DD-Lead; + provides strategic feedback on market and location viability. +- **NBH (National Business Head):** Holds final authority for approval or rejection of dealer + onboarding; reviews consolidated feedback from all levels. + +**2.1.3 Supporting Departments** + +- **Finance Team:** Reviews financial due diligence reports, validates F&F (Full and Final) + settlements, and manages monetary closure during offboarding. +- **Legal Team:** Reviews agreements, issues **Letters of Intent (LOI)** or **Termination Letters** , + and ensures all documentation aligns with company policy. +- **Brand Experience / Architecture Team:** Manages **EOR (Essential Operating** + **Requirements)** and ensures adherence to brand and infrastructure standards. + +**2.1.4 Dealers** + +Once a dealer is **successfully onboarded and activated in the system** , the Dealer role is enabled +with controlled, role-based access to initiate and track select lifecycle requests. This +enhancement introduces **structured self-service capabilities for dealers** , while ensuring all +actions remain governed by defined validations, internal reviews, and approval workflows as per +RE standards. + +The Dealer role is enabled to perform the following activities: + + +- **Resignation Initiation** + +``` +The dealer can initiate the resignation process directly through the portal , submit the +reason for exit, and track the status of the request across the defined review, clearance, +and closure stages. +``` +- **Relocation Request Submission** + +``` +The dealer can submit a relocation request in scenarios where there is an intent to shift +the dealership from the current location to a new proposed location. The request is +routed for internal feasibility assessment, validation, and management approval before +execution. +``` +- **Change in Constitution Request** + +``` +The dealer can initiate a Change in Constitution request to seek approval from RE +management for ownership or structural changes within the dealership. Upon approval, +the dealer may proceed with the legally compliant transition. +``` +``` +Supported Change in Constitution scenarios include: +``` +``` +o Proprietorship (Single Owner) โ†’ Partnership +o Proprietorship โ†’ LLP (Limited Liability Partnership) +o Proprietorship โ†’ Private Limited +o Partnership โ†’ LLP +o Partnership โ†’ Private Limited +o Private Limited โ†’ LLP +o Private Limited โ†’ Partnership +``` +All dealer-initiated requests are subject to **defined validations, mandatory document +submissions, role-based reviews, and approvals**. The dealerโ€™s access is **restricted to initiation, +document upload, and status visibility** , with **final decision-making authority retained by +authorized internal stakeholders of RE** + +### 2.2 External & Integrated Stakeholders + +**2.2.1 FDD (Financial Due Diligence Partner)** + +External agency responsible for assessing the applicantโ€™s financial health, verifying credentials, +and uploading due diligence reports into the system. + + +**2.2.2 Dealer / Applicant** +External user who applies for dealership, uploads required documents, participates in +interviews, and later accesses the portal for resignation or closure status. + +## 3 Definitions and Acronyms + +``` +Acronym Full Form / Description +RE Royal Enfield +DD Dealer Development +DD-AM Dealer Development โ€“ Area Manager +DD-ZM Dealer Development โ€“ Zonal Manager +DD-Lead Dealer Development โ€“ Lead +DD-Head Dealer Development โ€“ Head +RBM Regional Business Manager +ZBH Zonal Business Head +NBH National Business Head +ASM Area Sales Manager +FDD Financial Due Diligence (External Partner/Agency) +LOI Letter of Intent +EOR Essential Operating Requirements +LOA Letter of Appointment +F&F Full and Final (Dealer Settlement) +KT Matrix Evaluation Matrix used for scoring applicants +``` +## 4 HiFi Wireframes & Flow of Application + +HiFi Wireframes : https://mono-human-93592950.figma.site + +### 4.1 Dealer onboarding - Process Flow Overview + +The **Dealer Onboarding Workflow** outlines the end-to-end sequence through which a dealership +application progresses โ€” from initial registration to final inauguration and operational readiness. + + +**4.1.1 Step-by-Step Process Flow** + +``` +4.1.1.1 Application Initiation +``` +- The **applicant (dealer prospect)** submits an online application through the Dealer + Onboarding Portal. +- The system checks the **locationโ€™s availability** in the Royal Enfield dealership network: + o If the location has **no open opportunity** , a **Non-Opportunity Email** is triggered + automatically. + o If an opportunity exists, the applicant receives an **Opportunity Email** with login + credentials and a link to the **Dealer Questionnaire**. + +``` +4.1.1.2 Questionnaire Completion +``` +- The applicant fills out the **comprehensive questionnaire** covering business, infrastructure, + and financial readiness. +- The system auto-scores responses, generating a **Questionnaire Score** and **initial** + **ranking** for that applicant. + + +- Completed applications move to the **Admin review bucket**. +- The system shall trigger automated reminders to users for completing the + questionnaire. These **reminders will be sent through WhatsApp** , to ensure timely + submission. Reminder needs to be configured from Admin. + +``` +4.1.1.3 Admin Validation & Shortlisting +``` +- **DD-Admin** reviews all submitted applications and validates details and attached + documents. +- Based on eligibility, applications are either **shortlisted** for evaluation or **archived** for + future opportunities. +- Shortlisted applications are distributed to respective **zones or regions** for further + assessment. + +``` +4.1.1.4 Interview Evaluation (Multi-Level Process) +``` +- Admin schedules interviews in **Level 1** , **Level 2** , and **Level 3** , as applicable. +- Each interview can be **Virtual or Physical** , with calendar invites sent via Google Calendar. +- Evaluators at each level (DD-ZM, RBM, DD-Lead, ZBH, NBH, DD-Head) record their + feedback through: + o **KT Matrix Scoring** (quantitative) + o **Interview Feedback Form** (qualitative) +- The system consolidates panel feedback and generates an **AI-driven summary and** + **ranking** for decision support. + +``` +4.1.1.5 Financial Due Diligence (FDD) & Finance Review +``` +- Upon shortlisting, the application is assigned to the **FDD Team (external agency)** for + financial validation. +- FDD users, using SSO credentials, can: + o View assigned applications in a restricted interface. + o Upload FDD reports and add remarks in the **Work Notes** section. + o Flag cases of **non-responsiveness** or incomplete data, returning them to Admin. +- The **Finance team** reviews submitted FDD reports, validates findings, and decides + whether the application proceeds to **LOI approval**. The finance team is not the decision + maker for LOI Issuance. + +``` +4.1.1.6 LOI (Letter of Intent) Approval & Issuance +``` +- Based on Finance clearance, **DD-Head and NBH** review and approve the **LOI request**. +- The system tracks document approvals, timestamps, and supporting artefacts. + + +- Once approved, the LOI document is generated, uploaded, and shared **with the** + **applicant via official email communication** and not on WhatsApp +- Notification emails are triggered to all relevant stakeholders. + +``` +4.1.1.7 Dealer Code Generation & Setup +``` +- After LOI issuance, the **DD-Admin triggers** the Dealer Code creation process. Based on + this trigger, the **Dealer Code is created in the SAP Master** and **mapped to the applicant** + within the system. +- The code links all downstream modules, including Architectural, Statutory, and EOR + checklists. + +``` +4.1.1.8 Architectural Work & Statutory Documentation +``` +- Architectural activities are initiated (site plans, layout approvals, branding elements). +- The applicant and assigned Architecture Team upload documents, drawings, and + blueprints. +- In parallel, the applicant uploads **Statutory Documents** such as: + o GST certificate, PAN, Partnership Deed, Firm Registration, Rental/Lease + Agreement, etc. +- Each upload is timestamped and visible with file name, uploader, and document type. + +``` +4.1.1.9 Payment Verification & Finance Validation +``` +- Applicant uploads proof of advance payment or security deposit. +- The **Finance team** verifies payment details (transaction ID, amount, and bank record). +- Status is updated to **Verified** once the payment is reconciled. +- Verified payment triggers readiness for final operational setup. + +``` +4.1.1.10 Essential Operating Requirements (EOR) Checklist +``` +- All functional teams (Sales, Service, IT, Finance, Training, Architecture) verify their + respective readiness parameters. +- Progress is tracked through a **completion bar** until 100% EOR compliance is achieved. +- The **EOR checklist is initiated only after LOA issuance**. All functional teams verify their + respective readiness parameters, and progress is tracked until **100% EOR compliance** is + achieved. + +``` +4.1.1.11 LOA (Letter of Authorization) & Final Go-Live +``` +- After LOI issuance and Dealer Code generation, the **Letter of Authorization (LOA) is** + **generated and approved by NBH and DD-Head**. Upon successful LOA issuance, the + + +``` +application proceeds to the Essential Operating Requirements (EOR) checklist for final +readiness verification. +``` +- Final verification includes: + +``` +o EOR document review +o Brand readiness assessment +o Site validation and inspection +``` +- The **LOA** officially authorizes the dealership to operate under Royal Enfield. + +``` +4.1.1.12 Inauguration & Closure +``` +- Post-authorization, the **Inauguration** event is scheduled and logged. +- Completion of inauguration marks the dealership as **Active** in the system. + +``` +4.1.1.13 System-Driven Governance & Audit +``` +- Each stage automatically logs: + o User action, timestamp, and remarks + o Uploaded artefacts and version control + o Notifications sent and approvals received +- The entire lifecycle remains accessible under **Audit Trail** for future reference, compliance, + or offboarding workflows. + +### 4.2 Dealer Resignation โ€“ Process Flow Overview + +**4.2.1.1 Overview** + +``` +The Dealer Resignation Process manages the structured offboarding of a dealership initiated +by the dealer. The process begins when a dealer formally submits their resignation via +email to the Area Sales Manager (ASM) , after which the workflow transitions into the +system-managed approval sequence. +``` +``` +Dealer resignation requests are initiated by the dealer through the portal and subsequently +reviewed and processed by Admin, Finance, Legal, and relevant business stakeholders. +``` +``` +This flow ensures that each resignation is verified, discussed, and approved across all +required levels โ€” maintaining proper documentation, compliance, and traceability until the +final Legal Acceptance Letter is issued. +``` + +**4.2.2 Step-by-Step Process Flow** + +``` +4.2.2.1 Dealer Initiation +``` +- The dealer submits a **formal resignation email** on the dealershipโ€™s official letterhead to + the **ASM**. +- The resignation reason must be clearly stated (e.g., personal, financial, business + restructuring). +- The **dealer is provided portal access** to initiate the resignation request directly through + the system. The dealer submits resignation details, reason for exit, and proposed + timeline via the portal, after which the request enters the internal review and clearance + workflow. + +``` +4.2.2.2 ASM Review +``` +- The **ASM** reviews the dealerโ€™s resignation request and supporting letter. +- Uploads the **resignation email** and **dealerโ€™s letterhead document** onto the portal. +- Adds remarks summarizing the discussion and reason for resignation. +- Forwards the request to **RBM + DD-ZM** for evaluation. + +``` +4.2.2.3 RBM + DD-ZM Joint Evaluation +``` +- The **Regional Business Manager (RBM)** and **Dealer Development Zonal Manager (DD-** + **ZM)** review the uploaded documents. +- Conduct a joint discussion with the dealer to confirm the intent and understand any + issues. +- Uploads the **Minutes of Meeting (MOM)** or discussion summary. +- Adds comments and recommendations before forwarding to **Zonal Business Head** + **(ZBH)**. +- Actions available at this stage: + o **Approve** โ†’ Send forward for next-level review + o **Send Back for Clarification** โ†’ Returns to ASM + o **Withdraw** โ†’ Cancels the request (with remarks logged) + +``` +4.2.2.4 ZBH Review +``` +- The **Zonal Business Head (ZBH)** reviews the resignation summary and all remarks. +- Adds their comments and recommendations. +- Forwards the request to **DD-Lead** through the system. +- Worknote is updated automatically to reflect action and timestamp. +- The resignation request is reviewed by authorized business stakeholders, + including **RBM, ZBH, and DD-Head**. During the review stage, the **ZBH is authorized to** + + +``` +Send Back or Revoke the resignation request for clarification or correction. Send Back +actions are communicated to the dealer and internal teams through Work Notes , with +mandatory remarks captured for traceability. +``` +``` +4.2.2.5 DD-Lead Review +``` +- The **DD-Lead** consolidates all discussions, documents, and feedback. +- Prepares a **Resignation Presentation** with recommendations and supporting data. +- Uploads the presentation to the portal. +- Forwards the case to **NBH** for final decision. +- The resignation request is reviewed by the **DD-Lead and DD-Head**. At this stage, both + roles are authorized to **Send Back or Revoke** the resignation request for clarification, + correction, or reconsideration. **Send Back actions are communicated through Work** + **Notes** , with **mandatory remarks** recorded for audit and traceability. + +``` +4.2.2.6 NBH Final Approval +``` +- The **National Business Head (NBH)** reviews the entire resignation dossier. +- Adds final remarks with one of the following outcomes: + o **Approve** โ†’ Case moves automatically to Legal for letter issuance. + o **Send Back for Clarification** โ†’ Returns to DD-Lead or ZBH for revalidation. + o **Hold** โ†’ Temporarily pauses the process pending further discussion. +- Upon approval, the system triggers a **Worknote Notification** to DD-Lead, RBM, ZBH, and + Finance teams. +- The resignation request is reviewed by the **NBH** , who may **Approve, Send Back, or** + **Revoke** the request based on business considerations. Any **Send Back or Revoke action** + **must be accompanied by remarks recorded in Work Notes** , ensuring transparent + communication and governance. + +``` +4.2.2.7 Legal Acceptance Letter +``` +- Once approved by **NBH** , the request is **auto-assigned to the Legal team**. +- Legal verifies the uploaded resignation and issues a **Resignation Acceptance Letter**. +- The letter is uploaded to the portal, visible to all relevant personas including **DD-** + **Admin** and **DD-AM**. +- Legal can also raise clarifications through worknotes if required. +- Upon completion of all approvals, the **Legal team issues the official Resignation** + **Acceptance Letter** and shares it with the dealer through authorized communication + channels. + + +``` +4.2.2.8 DD-Admin Closure +``` +- The **DD-Admin** downloads and shares the final **Resignation Acceptance Letter** with the + dealer. +- Marks the resignation as completed and triggers the **F&F (Full and Final) process** by + forwarding the case to the Finance team. +- The **Full & Final (F&F) settlement process is initiated only on the Last Working Day** + **(LWD) of the dealership**. The system shall **enable and trigger the F&F workflow strictly** + **based on the LWD date** , and **not based on the resignation approval date**. + +### 4.3 Dealer Termination โ€“ Process Flow Overview + +``` +4.3.1.1 Overview +``` +``` +The Dealer Termination Process governs the structured offboarding of a dealership initiated +internally by Royal Enfield due to operational, contractual, or ethical concerns. +It ensures that any terminationโ€”whether due to working-capital issues, poor performance, +or unethical practices โ€”is investigated, documented, reviewed at multiple managerial levels, +and legally validated before final execution. The process maintains full transparency and +traceability through digital records, comments, and worknotes until the Termination +Letter is issued and the Full & Final (F&F) settlement begins. +``` +**4.3.2 Step-by-Step Process Flow** + +``` +4.3.2.1 ASM โ€“ Case Initiation +``` +- The **Area Sales Manager (ASM)** regularly visits dealers and records **Minutes of Meeting** + **(MOM)** for performance or compliance concerns. +- After two consecutive unsatisfactory commitments or escalations, the ASM initiates + a **Termination Request** in the portal. +- Fills all operational details (Dealer Code, LOI, LOA, Sales Data, etc.), selects + a **Termination Category** (Working Capital, Performance, Unethical Practice), and + uploads supporting documents (MOMs, commitments, dealer letters). +- Submits the case to **RBM + DD-ZM** for review. + +``` +4.3.2.2 RBM + DD-ZM Review +``` +- The **Regional Business Manager (RBM)** and **Dealer Development Zonal Manager (DD-** + **ZM)** jointly evaluate the case. + + +- Conduct a meeting with the dealer and record fresh MOMs; upload dealer + commitments on letterhead. +- Provide remarks and supporting evidence. +- Actions available: + o **Approve** โ†’ Forward to ZBH + o **Send Back for Clarification** โ†’ Returns to ASM with comments + o **Withdraw** โ†’ Terminates workflow with justification + +``` +4.3.2.3 ZBH Review +``` +- The **Zonal Business Head (ZBH)** reviews the full chronology (ASM visits, RBM/DD-ZM + remarks, uploaded MOMs). +- Validates escalation authenticity and dealer communication record. +- Adds remarks and forwards to **DD-Lead** for deeper review. +- The termination request is reviewed by the **ZBH** , who is authorized to **Approve, Send** + **Back, or Revoke** the termination request. **Send Back actions are communicated** + **through Work Notes** , with **mandatory remarks** recorded for traceability. + +``` +4.3.2.4 DD-Lead Review & Legal Assignment +``` +- The **DD-Lead** cross-verifies case chronology with all stakeholders (ASM, RBM, ZBH). +- Prepares a **Termination Presentation** summarizing facts, dealer history, and + recommendations. +- Assigns the case to **Legal Team** for inputs through the system (visible in worknotes). +- The termination request is reviewed by the **DD-Lead** , who is authorized to **Send Back or** + **Revoke** the termination request for clarification or reconsideration. All such actions + require **mandatory remarks captured in Work Notes**. + +``` +4.3.2.5 Legal Verification +``` +- The **Legal Team** reviews documentation, ensures contractual breaches are well- + supported, and checks all precedents. +- May raise queries via **Worknotes** or **Send Back** the case to DD-Lead for clarification. +- Once satisfied, forwards the verified case back to **DD-Lead** for next action. + +``` +4.3.2.6 DD-Lead โ†’ DD-Head Review +``` +- The **DD-Lead** attaches Legalโ€™s feedback and forwards the case to **DD-Head** for strategic + review. +- **DD-Head** validates the case, evaluates impact, and presents it to **National Business** + **Head (NBH)** for final business decision. + + +``` +4.3.2.7 NBH Evaluation +``` +- The **NBH** reviews all documentation and Legal remarks. +- May choose one of three actions: + o **Go Ahead** โ†’ Approve for issuance of **Show Cause Notice (SCN)** + o **Hold Decision** โ†’ Pause temporarily for further monitoring or negotiation + o **Raise Query** โ†’ Sends back to DD-Lead for additional input + +``` +4.3.2.8 Show Cause Notice (SCN) Issuance +``` +- Upon NBH approval, the system triggers Legal to prepare and issue the **SCN**. +- The **DD-Lead** formally shares the SCN with the dealer through **DD-Admin**. +- Dealer replies to the SCN by email or letter, which **DD-Admin uploads** to the portal. +- For termination cases, the **F&F settlement process is triggered only on the Last** + **Working Day (LWD)**. The system shall **control the F&F trigger based on the LWD date** , + irrespective of the termination approval date. + +``` +4.3.2.9 Evaluation of Dealer Response +``` +- The **DD-Lead** , **ZBH** , **RBM** , and **DD-Head** jointly review the dealerโ€™s SCN response. +- Uploads internal comments, Legal feedback, and recommendation for NBHโ€™s final + decision. + +``` +4.3.2.10 NBH Final Decision +``` +- The **NBH** reviews the compiled case with Legal advice and decides among: + o **Approve Termination** โ†’ Moves to CEO/CCO for confirmation + o **Reconsider** โ†’ Allow additional time or corrective action + o **Reject** โ†’ Case closed without termination + +``` +4.3.2.11 11. CEO & CCO Authorization +``` +- **CEO** and **Chief Commercial Officer (CCO)** review the NBH-approved termination. +- Provide authorization on the portal. +- Once signed off, the decision becomes final. + +``` +4.3.2.12 12. Legal Termination Letter +``` +- The **Legal Team** generates the **Termination Letter** to the portal. +- The letter is auto-visible to **DD-Lead** , **DD-Admin** , and **Finance**. +- A system notification is triggered to all linked personas. + + +``` +4.3.2.13 13. DD-Admin Communication & F&F Trigger +``` +- The **DD-Admin** shares the official **Termination Letter** with the dealer and field team. +- Marks the case as โ€œTerminatedโ€ in the portal. +- Forwards the case to **Finance** for **Full & Final Settlement** initiation. +- Updates the worknote with final remarks and due-date for settlement. + +### 4.4 Dealer Full & Final (F&F) Settlement โ€“ Process Flow + +``` +4.4.1.1 Overview +``` +The **Full & Final (F&F) Settlement Process** governs the financial closure of a dealership +following **Resignation** or **Termination**. +It ensures that all financial obligations between Royal Enfield and the dealer โ€” +including **security deposits, recoveries, payables, and department-wise dues** โ€” are +transparently reconciled, verified, and documented before closure. + +**4.4.2 Step-by-Step Process Flow** + +``` +4.4.2.1 F&F Initiation +``` +- Triggered automatically once the **Resignation Acceptance Letter** or **Termination** + **Letter** is uploaded by **Legal**. +- The **DD-Admin** or **DD-Lead** initiates the F&F case in the **Finance Dashboard** , which + creates a unique **FNF Case ID** linked to the dealer code. +- The system auto-fetches dealer details, associated documents, resignation/termination + date, and due dates. +- Notification is sent to the **Finance Team** and all functional departments to begin the + clearance process. + +``` +4.4.2.2 Department-wise Response Collection +``` +- The system automatically prompts all mapped **functional departments (16 in total)** to + submit their clearance inputs โ€” including NOC, payables, recoveries, and remarks. +- Each department updates: + o Financial dues (if any) + o Clearance confirmation (NOC) + o Supporting document uploads (e.g., debit note, invoice copy) +- The system dynamically updates progress (e.g., _12/16 Departments Responded_ ) with + color-coded indicators: + o ๐ŸŸข **No Dues** โ€“ Cleared + + +``` +o ๐Ÿ”ด Dues Pending โ€“ Outstanding financial liability +o โšช Pending โ€“ Awaiting department input +``` +- SLA-based reminders are auto-triggered for pending responses nearing the deadline. + +``` +4.4.2.3 Finance Summary Consolidation +``` +- Once all departments respond, the **DD-Admin Team** consolidates inputs into the **Final** + **F&F Summary Sheet** , which consists of: + o **Payables to Dealer** (e.g., refundable deposits, reimbursements) + o **Receivables from Dealer** (e.g., outstanding invoices, recoveries) + o **Deductions** (policy penalties, non-compliance adjustments) +- The system automatically calculates: + - Net Settlement = Total Payables โ€“ Total Receivables โ€“ Total Deductions +- Finance reviews and adjusts entries as needed, attaching relevant proofs for + transparency. +- Status updates to _Finance Summary Prepared_ once complete. + +``` +4.4.2.4 Internal Review & Clarification +``` +- The **Finance Team** may use the **Work Note** section to raise clarifications to **DD-** + **Lead** , **Legal** , or concerned departments. +- If discrepancies exist (e.g., mismatched values or missing NOCs), the case remains _Under_ + _Clarification_ until resolved. +- Once validated, Finance locks the summary for further edits. + +``` +4.4.2.5 Dealer Discussion & Acknowledgment +``` +- The **Finance Team** , along with **Legal** and **DD-Lead** , discusses the settlement summary + with the dealer. +- Dealer acknowledgment is captured either via written confirmation or attached email + communication. +- The case then proceeds for **Final Finance Approval**. + +``` +4.4.2.6 Final Finance Approval & Payment Processing +``` +- The **Finance Team** reviews the approved summary and enters payment or recovery + details: + o **Transaction Type:** RTGS / NEFT / Cheque + o **Transaction ID & Date** + o **Bank Name & Account Details** (auto-fetched from dealer profile) + o **Settlement Remarks** +- Finance takes one of three actions: + + +``` +o Approve Settlement โ†’ Marks the case as โ€œFinance Approved.โ€ +o Request Clarification โ†’ Sends query to DD-Lead or Admin. +o Reject Summary โ†’ Returns for re-verification. +``` +- Upon approval, notifications are sent to DD-Admin and Legal for record update. + +``` +4.4.2.7 F&F Completion & Closure +``` +- Once approved, the case is automatically marked **Completed** , and the **Finance** + **Dashboard** updates status as _F&F Closed_. +- The **Settlement Proof** (e.g., payment confirmation or recovery adjustment) is uploaded + by Finance. +- The **DD-Admin** communicates official closure to the dealer and archives all artefacts. +- System triggers final alerts to DD-Lead, NBH, and Legal confirming completion. +- The case is archived in the **Audit Trail** for future reference. + +### 4.5 Finance Team โ€“ Process Flow + +``` +4.5.1.1 Overview +``` +The **Finance Team Process Flow** governs all financial activities related to dealer lifecycle +management โ€” from **security deposit validation at onboarding** to **final settlement at +resignation or termination**. +It ensures complete financial traceability, proper verification of payments, and compliance with +Royal Enfieldโ€™s financial governance standards. +The process flow integrates with **Admin, Legal, Dealer Development (DD)** , and **Departmental +Modules** , ensuring accurate financial updates and timely closure of all financial transactions. + +**4.5.2 Step-by-Step Process Flow** + +``` +4.5.2.1 Security Deposit Validation (Onboarding Stage) +``` +- **Trigger:** + Initiated when a new dealerโ€™s onboarding application reaches the Finance stage after + DD approval. +- **Action:** + The **Finance Team** verifies the **Security Deposit** payment made by the dealer + via **RTGS/NEFT** or other approved channels. +- **Outcome:** + o Verified deposits are marked as _Approved_ , triggering system notifications to DD- + Admin and DD-Lead. + + +``` +o The verified payment data is stored permanently in the dealerโ€™s financial profile +for audit and reference. +``` +``` +4.5.2.2 Financial Summary Preparation +``` +- **Action:** + Once departmental inputs are received, Finance consolidates all data into the **F&F** + **Summary Sheet**. +- **System Steps:** + o Segregates entries under: + โ–ช **Payables to Dealer** (e.g., refundable deposits, reimbursements) + โ–ช **Receivables from Dealer** (e.g., outstanding payments, penalties) + โ–ช **Deductions** (e.g., policy recoveries, warranty holdbacks) + o The system auto-calculates: + o Net Settlement = Total Payables โ€“ Total Receivables โ€“ Deductions + o Finance validates each record, uploads supporting documents (receipts, invoices, + credit notes), and adds remarks. +- **Outcome:** + The computed **Net Settlement Amount** is reflected in the dashboard, categorized + as _Payable to Dealer_ or _Recoverable from Dealer_. + +``` +4.5.2.3 Internal Clarification & Approval +``` +- **Action:** + Finance initiates clarification rounds with departments or DD-Lead for mismatched data. +- **System Steps:** + o Uses the **Work Notes** section for comments, tagging users like _@DD-_ + _Lead_ , _@Legal_ , or _@Admin_. + o Tracks status as _Pending Clarification_ until resolved. + o After reconciliation, Finance locks the summary and updates case status + to _Ready for Approval_. + +``` +4.5.2.4 Final Review & Dealer Confirmation +``` +- **Action:** + Finance conducts an internal review of the consolidated settlement and initiates a + financial discussion with the dealer. +- **System Steps:** + o Reviews summary details on-screen with Legal and DD-Lead. + o Records dealerโ€™s acknowledgment via Work Note or attached email + confirmation. + o Once confirmed, proceeds to payment verification. + + +``` +4.5.2.5 Payment Processing & Record Update +``` +- **Action:** + Finance executes the financial transaction (payment to or recovery from dealer). +- **System Steps:** + o Enters **Mode of Payment** , **Transaction Reference Number** , **Date** , and **Remarks**. + o Uploads proof of payment (RTGS confirmation or bank statement). + o Marks case as _Finance Approved_ and sends completion notification to DD-Admin + and Legal. + o System automatically updates the **Progress Timeline** and **Audit Trail**. + +``` +4.5.2.6 F&F Completion & Closure +``` +- **Action:** + Finance reviews all entries, confirms ledger reconciliations, and marks case + as **Completed**. +- **System Steps:** + o Locks financial data and supporting artefacts. + o Status changes to _Closed โ€“ F&F Completed_. + o Final confirmation sent to all stakeholders โ€” DD-Lead, NBH, DD-Head, Legal, and + DD-Admin. + o Finance Dashboard updates counters under โ€œCompleted Cases.โ€ + +## 5 System Features & Requirements + +Here, we describe the **system features** along with their respective **Width** and **Depth** to provide +complete visibility of each requirement. + +The **Width** defines the **functional coverage** of a feature โ€” outlining what the feature does, +its **boundaries, use cases, and user interactions**. It answers the question: _โ€œWhat scenarios and +actions are covered by this feature?โ€_ + +The **Depth** captures the **operational and behavioral details** โ€” describing how the feature +behaves through its **logic, workflow, system responses, and edge-case handling**. It answers the +question: _โ€œHow does the system execute and respond in these scenarios?โ€_ + + +## 6 Dealer onboarding + +### 6.1 Dealership Application Form + +**6.1.1 Functionality Scope** + +The **Dealer Application Form** is the entry point for individuals or businesses applying to become +an authorized **Royal Enfield dealer**. This form captures all essential applicant details to initiate +the onboarding workflow. It ensures structured data collection, validation, and consent before +the request enters the evaluation process. + + +**6.1.2 Width** + +- Accessible from the **Royal Enfield official website** and internal campaigns (QR-based or + direct link). +- Available as part of the **Dealer Onboarding module** under the section _โ€œApply for_ + _Dealership.โ€_ +- On successful submission, the application is routed to the **DD-Admin** and **respective** + **zonal evaluation team** for screening. + +**6.1.3 Depth** + +- The form captures applicant identity, contact, business, and location information through + mandatory fields such as: + o **Full Name** , **Mobile Number** , **Email Address** , **Age** + o **Country** , **State** , **District** , **Pincode** + o **Interested City for Dealership** , **Company Name** , **Education Qualification** + o **How did you hear about us?** + o Ownership details โ€” _โ€œDo you own a Royal Enfield?โ€_ and _โ€œAre you an existing_ + _dealer/vendor?โ€_ + o **Address** and **Description** fields capturing business background, experience, and + dealership intent +- The form enforces **mandatory validations** (e.g., email format, mobile number pattern, + field completeness) before submission. +- Applicants must acknowledge the **Terms and Conditions** via a mandatory consent + checkbox, ensuring compliance with REโ€™s data privacy policy. +- Upon clicking **Submit Application** , data is securely stored in the system database and + auto-routed to the assigned **region/zone hierarchy**. +- The system sends an acknowledgment **notification to the applicant via email and** + **registered mobile number**. Mobile-based notifications will be delivered through + WhatsApp. +- **Form will be having a disclaimer:** A consent checkbox is mandatory in the + application form. The applicant must acknowledge this disclaimer before + submission: _โ€œBy submitting this form, you agree to our privacy policy and terms of_ + _service. We will use your information to process your dealership application.โ€_ + +**6.1.4 Personas-Wise Accessibility & Visibility** + +``` +Persona Accessibility Visibility Scope +Applicant / Dealer +Prospect +``` +``` +Can view and fill the form; required to submit +all mandatory details +``` +``` +Full visibility for own +submission +``` + +### 6.2 SSO Login + +**6.2.1 Functionality Scope** + +The **User Authentication & Secure Login** module ensures controlled access to the **Dealer +Onboarding and Offboarding System** through **Royal Enfieldโ€™s Single Sign-On (SSO) +bridge** integrated with **Active Directory (AD)**. This guarantees that only **authorized RE +employees** can access the platform while maintaining identity consistency across all RE +applications. The feature upholds organizational standards of **security, traceability, and role- +based access control (RBAC)**. + +**6.2.2 Width** + +- The login interface is the **entry point** to the system and is accessible via the internal RE + portal or direct system URL. +- It contains the following key fields and actions: + o **Email Address** (RE domain-based ID) + o **Password** (validated via Active Directory) + o **Remember Me** (optional session retention) + o **Forgot Password** (redirects to REโ€™s password reset service via SSO) +- Once authenticated, users are redirected to their **personalized dashboard** based on role + and access level defined in RBAC. + + +**6.2.3 Depth** + +- The system leverages **REโ€™s enterprise SSO framework** for identity validation and token- + based session management. +- User credentials are not stored within the application; authentication tokens are + validated through **Active Directory** integration. +- Upon successful login, the system fetches user metadata (name, department, role, region, + and zone) to determine module visibility and permissions. +- **Role-Based Access Control (RBAC)** defines feature-level authorization for each user + category (e.g., DD-Admin, DD-ZM, RBM, Finance, Legal). +- Unauthorized users or non-RE email domains are denied access and redirected to an error + page stating: _โ€œAccess restricted to authorized Royal Enfield personnel only.โ€_ +- User sessions are automatically logged out after 30 mins of inactivity period + +**6.2.4 Flow** + +``` +Step Source โ†’ Destination Description / Action +1 User โ†’ Login Screen User enters RE email ID and password. +``` +``` +2 +``` +``` +Login Screen โ†’ SSO +Bridge Credentials validated via Active Directory.^ +3 SSO โ†’ System Authentication token passed back to the application. +4 System โ†’ RBAC Engine User role and permissions are identified. +``` +``` +5 System โ†’ User Dashboard User redirected to personalized dashboard based on access +level. +``` +**6.2.5 Personas-Wise Accessibility & Visibility** + +``` +Persona Accessibility Visibility Scope +All RE Employees Login access via SSO +bridge +``` +``` +Limited to assigned modules post-login +``` +``` +External Users (Dealers, +FDD Partners) +``` +``` +No direct access via +RE SSO +``` +``` +Access provided through secure external +login URLs when applicable +``` +**Dependency** + +- SSO implementation guide and sample users are required. + + +### 6.3 Dashboard + +The **Dashboard** is an open element that is **currently under discussion** and will be finalized in later +phases. It is expected to serve as a central view for users to monitor workflow status, pending +approvals, and key metrics once its structure and content are defined. + +### 6.4 Opportunity & Non Opportunity + +**6.4.1 Functionality Scope** + +The **Opportunity & Non-Opportunity Management** module classifies all dealer applications +received through the **Dealer Application Form** into two distinct categories as +**Opportunity** and **Non-Opportunity**. This classification is determined automatically based on the +applicantโ€™s **preferred dealership location** and the **current availability** of opportunities defined +in the system. In certain cases, an applicant may express interest in a specific location (e.g., +Chennai) where an opportunity is not currently open. Such applications will remain on hold and +can be reactivated or re-sent once the opportunity for that location becomes available. The + + +system shall allow the applicant to reinitiate the opportunity request without re-entering all +previous details. The module ensures that every application is properly acknowledged and +routed for further processing or stored for future reference, maintaining transparency and +traceability in applicant communication. + +**6.4.2 Width** + +2.1 The module appears under the **Dealer Onboarding** workspace with two key views: + +- **Opportunity Requests** โ€“ Displays all applications where dealership opportunities are + currently open in the requested region. +- **Non-Opportunity Requests** โ€“ Captures applications for regions where dealership + opportunities are not available at the time of submission. + +2.2 Both views can be accessed by **DD-Admin, DD-Lead, and DD-ZM** users based on their defined +RBAC access levels. + +2.3 Each application record displays critical information such as **Registration Number, Name, +Preferred Location, Status, Applicant Location, Progress, and Application Date**. + +**6.4.3 Depth** + +3.1 When a dealer submits an application, the system checks the **preferred city and +region** against the **Opportunity Town Master** configured by the admin. + +3.2 Based on this validation, the following actions occur automatically: + +1. If an **opportunity exists** , the applicant receives an **Opportunity Email** , confirming that + their location is currently under consideration. +2. If **no opportunity exists** , a **Non-Opportunity Email** is triggered, informing the applicant + that the region is closed for dealership openings but that their information will be + retained for future reference so in case any opportunity pops up later, he can be + contacted. + 3.3 All submitted applications initially land with the **Admin** , who performs a preliminary + validation and shortlist them into appropriate zonal or regional queues. + 3.5 Both Opportunity and Non-Opportunity records are time-stamped and retained for + audit visibility and future lead generation reference. + + +**6.4.4 Flow** + +``` +Step Source โ†’ Destination Description / Action +``` +``` +1 +``` +``` +Dealer Applicant โ†’ Application +Form +``` +``` +Applicant submits dealership request with preferred +city. +``` +``` +2 System โ†’ Opportunity Validation Engine System verifies if the preferred city exists in the Opportunity Master. +``` +``` +3 Validation Engine โ†’ Applicant Sends Opportunity or Non-Opportunity Email based on +result. +``` +``` +4 System โ†’ DD-Admin +``` +``` +All applications (both categories) are routed to Admin +for validation. +``` +``` +5 DD(respective Zone)-Admin โ†’ DD - ZM / DD-Lead^ Admin distributes qualified opportunities to respective zones for next-level evaluation. +``` +**6.4.5 Personas-Wise Accessibility & Visibility** + +``` +Persona Accessibility Visibility Scope +Applicant Receives automated acknowledgment and opportunity +status email +``` +``` +None beyond +email notification +DD-Admin Full access to both Opportunity and Non-Opportunity +queues; performs initial validation and assignment +``` +``` +Complete +visibility +DD-ZM / DD- +Lead / RBM +``` +``` +View and act on assigned Opportunity Requests only Restricted to +assigned zone or +region +System +(Automation +Layer) +``` +``` +Performs validation and triggers automated +notifications. +The system triggers automated notifications across +configured channels, including email and WhatsApp , +based on workflow events and application status +changes. +``` +``` +Background +execution only +``` + +### 6.5 Questionnaire Response + +The **Questionnaire Response & Scoring** module captures, displays, and evaluates responses +submitted by dealer applicants during the onboarding process. It enables Admin to view all +applicant answers in a structured format with predefined scoring and section-wise weightage. +The objective is to ensure that each applicant is evaluated objectively and consistently based on +parameters such as personal background, financial capacity, and business readiness. The overall +questionnaire score directly contributes to the applicantโ€™s ranking within the region or city for +fair selection and further shortlisting. Questions are to be managed from Admin with versions. + +**6.5.1 Width** + +- Accessible under **Application Details โ†’ Questionnaire Response tab**. +- Displays categorized sections such as: + o Personal Information + o Financial Information + o Business Information (if applicable) +- Each response card shows the **question, applicantโ€™s answer, and the score obtained** out + of the assigned weightage (e.g., 5/5, 8/10). +- The top-right corner displays the **aggregate questionnaire score** , expressed as a numeric + total (e.g., _78/100_ ). + +**6.5.2 Depth** + +- The questionnaire is to be created by Admin which will be common for all. There will be + versioning of it in case the questions are added or changed over time. + + +- Each question carries a **predefined weightage** configured by the Admin under the + Questionnaire Master. +- The system automatically calculates the total score once the applicant submits the + questionnaire. +- Evaluation parameters include as example: +- https://docs.google.com/forms/d/1YfTGFNx4zrul0YkJmCp7P0jyJKTiPRMxFvgZE8pmO9g/ + edit +- + o **Personal Details:** State, Age, Qualification, and Business Experience + o **Financial Details:** Net worth, investment capacity, and funding sources + o **Business Readiness:** Infrastructure preparedness, local market knowledge, and + management strength +- The cumulative score determines the **applicantโ€™s rank** relative to others in the same + location. +- All responses and scores are stored ensuring transparency and audit traceability. +- Any updates to the scoring model or questionnaire format are logged and version-tagged + for future reference. + +**6.5.3 Personas-Wise Accessibility & Visibility** + +``` +Persona Accessibility Visibility Scope +Applicant Can fill and submit questionnaire during +onboarding. +``` +``` +Own submission only. +``` +``` +DD-Admin Can view all applicant responses and manage +questionnaire versions. +``` +``` +Full visibility. +``` +``` +DD-ZM / DD-Lead / +RBM +``` +``` +Can view applicant responses and scores for +evaluation or ranking purposes. +``` +``` +Restricted to +assigned regions. +System +(Automation Layer) +``` +``` +Performs automatic scoring, rank generation, +and data versioning. +``` +``` +Background +execution. +``` + +### 6.6 Shortlisting Process + +**6.6.1 Functionality Scope** + +This functionality allows the **DD-Admin** to review all dealer applications received through the +questionnaire responses and shortlist the qualified ones for further evaluation. Admin can view applicant +details, compare preferred and available locations, and assign shortlisted applications to the +respective **DD-ZM** and **RBM** for next-level processing. Each shortlisted record can include remarks +capturing the rationale for selection or any special observation. Once assigned, the shortlisted +applications move to the **Dealership Requests** page for regional evaluation and workflow initiation. + +**6.6.2 Width** + +- Display of all received applications in a tabular view. +- Search and filter for quick reference. +- Ability to select multiple applications for shortlisting. +- Option to assign shortlisted applications to **DD-ZM** and **RBM** users via email ID. +- Field to capture optional remarks during shortlisting. +- Confirmation dialog before final submission. +- Status transition of applications from _โ€œSubmittedโ€_ to _โ€œShortlistedโ€_. +- Automated notification and dashboard update for assigned users. + +**6.6.3 Depth** + +- Admin can evaluate each application based on scoring, questionnaire performance, and location + preference before shortlisting. + + +- The shortlisted applications are automatically linked to both **DD-ZM** and **RBM** under their zonal + purview. +- Each assignment creates an audit entry with timestamp, assigning authority, and remarks for + traceability. +- Only valid user email IDs mapped to internal roles (DD-ZM/RBM) can be selected. +- System triggers workflow notifications and emails to the assigned reviewers with application + references. +- Applications remain editable for Admin until confirmation of shortlisting. +- Assigned users can view assigned applications in their dashboards for evaluation. + +**6.6.4 Personas-wise Accessibility & Visibility** + +``` +Persona Accessibility / Actions Visibility +DD-Admin Can view all received applications, shortlist eligible ones, +enter remarks, and assign to DD-ZM & RBM. +``` +``` +Full access to all +applications and history. +DD-ZM Can view shortlisted applications assigned to their zone +and proceed with evaluation. +``` +``` +Zone-wise shortlisted +applications. +RBM Can view and evaluate shortlisted applications for their +respective regions. +``` +``` +Region-wise shortlisted +applications. +ZBH / DD-Lead +/ NBH +``` +``` +Can view summary of shortlisted applicants for +monitoring and audit. +``` +``` +Read-only access. +``` +``` +Applicant Can view the updated application status as Shortlisted in +their dashboard. +``` +``` +Own application only. +``` +### 6.7 Shortlisted Applicants + +**6.7.1 Functionality Scope** + +The **Dealership Requests** screen serves as a centralized workspace for managing and tracking +all **shortlisted dealer applications** that have successfully progressed through initial evaluation +stages. It consolidates applications that are ready for multi-level approval and further processing, +ensuring clear visibility of their current stage, location, and progress. This screen allows internal +users such as **DD-Lead, DD-ZM, and DD-Admin** to monitor real-time progress, review application + + +details, and proceed with subsequent workflow actions such as interviews, EOR tracking, and +final approval. + +**6.7.2 Width** + +- Located under the **Dealer Onboarding module โ†’ Dealership Requests**. +- Accessible to internal stakeholders involved in the approval cycle. +- Displays tabular data including: + o ID and Applicant Name + o Preferred and Applicant Location + o Status (e.g., _Shortlisted, Level 1 Pending, Level 2 Approved, EOR In Progress_ ) + o Progress percentage bar + o Date of Application and View Action Button +- Includes search, filter, and export options to enhance navigation and reporting efficiency. + +**6.7.3 Depth** + +- The screen lists only those applications that have been **shortlisted** from the Opportunity + stage or advanced through earlier workflow levels. +- Each record dynamically reflects its **workflow status** , For example: + o _Shortlisted_ โ€“ application qualified for initial evaluation. + o _Level 1 Pending_ โ€“ awaiting review by DD-ZM and RBM. + o _Level 2 Approved_ โ€“ cleared by DD-Lead and ZBH. + o _Level 3 Pending -_ awaiting review by NBH + Head. + o _EOR In Progress_ โ€“ dealership architecture and statutory stages initiated. +- The **Progress bar** visually indicates the percentage completion of each applicationโ€™s end- + to-end journey. +- Users can click **View** to open the Application Detail View for in-depth review and decision- + making. +- All updates and transitions in application status are **system-driven** , ensuring traceability + and eliminating manual tracking. + +**6.7.4 Personas-Wise Accessibility & Visibility** + +``` +Persona Accessibility Visibility +Scope +DD-Admin Can view and monitor all shortlisted applications across +zones. +``` +``` +Full system +visibility. +DD-ZM / RBM Can access applications belonging to their assigned region +for Level 1 evaluation. +``` +``` +Regional +visibility. +DD-Lead / ZBH Can review and approve Level 2 applications. Zone-specific +visibility. +``` + +``` +System +(Automation +Layer) +``` +``` +Updates workflow status and progress dynamically. Background +execution. +``` +``` +NBH The NBH oversees strategic decision-making across +dealer onboarding, resignation, and termination +workflows, and participates in critical approval and +governance checkpoints. +``` +### 6.8 Application Detail View + +**6.8.1 Functionality Scope** + +The **Application Detail View** provides a consolidated and comprehensive overview of a dealer +applicantโ€™s profile for internal reviewers such as the **DD-Admin, DD-ZM, and DD-Lead**. It +centralizes all relevant information submitted by the applicant and derived from the +questionnaire evaluation to support ranking and decision-making. This screen allows authorized + + +users to review applicant details, track progress, view ranks within the same city or region, and +take context-specific actions such as approving, rejecting, assigning, or scheduling interviews. + +**6.8.2 Width** + +- Located within the **Dealer Onboarding Module โ†’ Opportunity Requests โ†’ Application** + **Detail** view. +- Accessible after selecting any applicant record from the Opportunity Requests list. +- Displays sections such as **Applicant Information** , **Summary** , **Actions** , and **Work Notes**. + +**6.8.3 Depth** + +- The **Applicant Information** section displays essential profile details including: + o Full Name, Email, Mobile Number, and Age + o Education Qualification and Past Experience + o Preferred Location, Residential Address, and Business Address + o Questionnaire Score (auto-calculated from applicantโ€™s responses) +- The **Summary Panel** on the right-hand side presents: + o Registration ID and Current Status of the application + o Applicantโ€™s **Rank** relative to other candidates in the same city, based on + questionnaire scores + o Visual **Progress Bar** indicating the current stage in the onboarding lifecycle +- The **Actions Panel** allows the reviewer to: + o Approve or Reject the application at the respective level based on RBAC + o Add **Work Notes** or comments visible to internal users + o Schedule an Interview or Assign the application to another reviewer +- The **Work Notes Section** provides an audit of all communications and remarks exchanged + during evaluation, maintaining process traceability. +- The **Ranking** is dynamically calculated based on the questionnaireโ€™s total score within + each city, ensuring comparative evaluation transparency. + +**6.8.4 Personas-Wise Accessibility & Visibility** + +``` +Persona Accessibility Visibility Scope +DD-Admin Full access to view, edit, assign, and update +applicant details +``` +``` +Complete visibility +``` +``` +DD-ZM / DD-Lead / +RBM +``` +``` +Can review applicant profile, approve/reject, +add notes, and view rank +``` +``` +Region- or city-specific +visibility +System (Automation +Layer) +``` +``` +Auto-calculates rank and score; updates +status dynamically +``` +``` +Background operation +``` + +### 6.9 Interview Scheduling & Coordination + +The **Interview Scheduling & Coordination** module enables the **Admin** to set up, manage, and +communicate interview sessions between dealership applicants and Royal Enfieldโ€™s evaluation +panel members. It supports scheduling across **Level 1, Level 2, and Level 3** interviews, ensuring +structured coordination and traceability. The feature provides flexibility to conduct interviews in +either **virtual** or **physical** mode and ensures timely notification of all stakeholders through +automated Google calendar invites. The goal is to streamline interview planning, eliminate +manual follow-ups, and ensure every shortlisted applicant is evaluated by the appropriate +authority as per the defined onboarding workflow. + +**6.9.1 Width** + +- Accessible under **Application Detail View โ†’ Schedule Interview**. +- Managed exclusively by **DD-Admin** for all interview levels. +- The form includes: + o **Interview Type:** Level 1, Level 2, or Level 3 + o **Interview Mode:** Virtual (via Google Meet) or Physical (on-site venue) + o **Date & Time:** Calendar-based selection + o **Participants:** Field for adding evaluator email addresses (comma-separated) + o **Meeting Link / Location:** Manually entered by Admin based on interview mode +- Once scheduled, the system sends **Google Calendar invites** to all participants and the + applicant with the interview details embedded. + +**6.9.2 Depth** + +- **Interview Levels:** + o _Level 1:_ Conducted by DD-ZM and RBM for preliminary evaluation and KT Matrix + review. + o _Level 2:_ Conducted by DD-Lead and ZBH for business and operational assessment. + o _Level 3:_ Conducted by NBH and DD-Head as the final approval stage. + + +- The **Admin** manually adds the **Google Meet link** (for virtual interviews) or **venue** + **address** (for physical sessions) before scheduling. +- After scheduling, a **Google Calendar invitation** is automatically triggered to all + participants and the applicant, containing the meeting details, mode, and timing. +- The system automatically updates the **Application Timeline** to reflect the scheduled + interview with date, time, and mode tags. +- All scheduling actions, including edits or reschedules, are captured in the **Audit Trail** for + traceability. +- The feature ensures complete visibility and coordination across all levels of the interview + process. + +**6.9.3 Personas-Wise Accessibility & Visibility** + +``` +Persona Accessibility Visibility Scope +DD-Admin Can create, edit, and manage interview +schedules for all levels. +The DD ASM is responsible for interview +scheduling and coordination with dealer +prospects. +The Admin role does not participate in +interview scheduling and only facilitates +system - level access and configuration. +``` +``` +Full visibility. +``` +#### DD-ZM / RBM / DD- + +``` +Lead / ZBH / NBH / DD- +Head +``` +``` +Receive Google Calendar invitations with +meeting details; can view schedule over +Google Calendar +``` +``` +Restricted to +assigned level and +applicant. +Applicant / Dealer +Prospect +``` +``` +Receives interview schedule via email and +Google Calendar invite with meeting details. +``` +``` +View-only for own +interview details. +System (Automation +Layer) +``` +``` +Sends Google Calendar invites, updates +application status, and logs actions in the Audit +Trail. +``` +``` +Background +execution. +``` +### 6.10 Interview Evaluation & Feedback Management + + +**6.10.1 Functionality Scope** + +The **Interview Evaluation & Feedback Management** module enables Royal Enfieldโ€™s internal +panel members to evaluate dealership applicants through structured, multi-level assessments. It +captures both **quantitative scoring** (via the KT Matrix) and **qualitative insights** (via structured +feedback forms) to ensure a balanced and transparent evaluation process. This module +standardizes the review and ranking procedure across **three interview levels** , integrates **AI- +assisted recommendations** , and provides consolidated visibility for final approval. It ensures that +each shortlisted applicant is assessed fairly, with complete traceability from panel feedback to +final NBH decision. + +**6.10.2 Width** + +- Accessible from **Application Detail View โ†’ Interview Evaluation** section. +- Used during all **three interview levels** : + o **Level 1:** DD-ZM + RBM โ€“ Initial evaluation and KT Matrix scoring. + o **Level 2:** DD-Lead + ZBH โ€“ Strategic and operational capability assessment. + o **Level 3:** NBH + DD-Head โ€“ Final review and approval decision. + + +- Comprises two core components: + o **KT Matrix Evaluation Form** โ€“ Records structured scores across weighted + parameters. + o **Interview Feedback Form** โ€“ Captures remarks, performance summaries, and + recommendations. +- Once submitted, all feedback becomes read-only and is logged in the **Audit Trail** for + compliance and future reference. + +**6.10.3 Depth** + +- **Multi-Level Interview Workflow:** + o Applicants progress sequentially through Level 1, Level 2, and Level 3. + o Interviews may be conducted **virtually via Google Meet** or **physically at** + **designated venues**. + o **DD-Admin** or **DD-Head** schedules interviews and sends **Google Calendar** + **invites** to all panelists and the applicant. + o The **Google Meet link** (for virtual sessions) or **venue address** (for physical sessions) + is entered manually during scheduling. +- **KT Matrix Evaluation & Ranking:** + o Panelists evaluate applicants using the configurable **KT Matrix** , which contains + weighted parameters contributing to a total of 100%. + o Evaluation fields include: + โ–ช Age and Qualification + โ–ช Local Knowledge and Influence + โ–ช Passion for Royal Enfield and Riding + โ–ช Business Acumen and Investment Capacity + โ–ช Base Location vs Applied Location + โ–ช Property Ownership, Time Availability, and Future Expansion Plans + o The system auto-calculates total KT Matrix scores and updates the **applicantโ€™s** + **rank** within the city or region. + o Ranking updates dynamically as evaluations are submitted, ensuring real-time + comparison across applicants. +- **Interview Feedback Form:** + o Enables panelists to provide qualitative assessments beyond numeric scoring. + o Key feedback areas include: + โ–ช Strategic Vision and Market Understanding + โ–ช Management Capabilities + โ–ช Operational Readiness + โ–ช Key Strengths and Areas of Concern + o Each panelist submits an **Overall Performance Score** and **Final** + **Recommendation** ( _Approve_ , _Reject_ , or _Hold_ ). + + +``` +o Remarks are consolidated for transparency and displayed in the application +timeline. +o All records are time-stamped and locked post-submission to preserve integrity. +``` +**6.10.4 Panel Feedback & AI Recommendation** + +- After each interview round, all **panel members except NBH** record their individual + remarks, ratings, and recommendations in the system using the **Dealer Interview** + **Recommendation Sheet (For CCO_NBH Approval.xlsx)** format. +- The **standard panel composition** includes: + o Applicant (Prospect) + o RBM (Regional Business Manager) + o ZBH (Zonal Business Head) + o DD-ZM (Zonal Manager) + o DD-Lead + o DD-Head + o NBH (National Business Head โ€“ Final Approver) +- Each panelist logs their evaluation covering both **quantitative scores** (via KT Matrix) + and **qualitative insights** (via feedback forms). +- Once all inputs are submitted, the system consolidates feedback and scoring data, then + passes it to the **AI engine (Gemini API)**. +- The AI processes the inputs and generates a **two- to three-line summarized** + **recommendation** that highlights: + o Consensus trend across panelists + o Applicantโ€™s key strengths and differentiators + o Potential concerns or areas for improvement +- The **AI-generated summary** is then presented to the **NBH** in editable format, allowing + review and refinement before finalizing the decision. +- The **NBH** may: + o Approve the AI-generated recommendation directly, or + o Modify the summary to incorporate additional observations before final + submission. +- This process ensures every recommendation reflects **data-backed consensus, AI-** + **supported insights, and human judgment** , maintaining full transparency, accountability, + and audit readiness. + +**6.10.5 Personas-Wise Accessibility & Visibility** + +``` +Persona Accessibility Visibility Scope +DD-ZM / RBM +(Level 1) +``` +``` +Fill KT Matrix, record evaluation, remarks, and +recommendations. Filled by both. +``` +``` +Region-specific +visibility. +``` + +``` +DD-Lead / ZBH +(Level 2) +``` +``` +Assess business strategy and operations, provide +structured feedback and score. Filled by both +``` +``` +Zone-level visibility. +``` +``` +NBH / DD-Head +(Level 3) +``` +``` +Review consolidated feedback, AI summary, and +finalize applicant decision. +``` +``` +Full visibility. +``` +``` +DD-Admin Monitor feedback submissions and completeness +across all interview levels. +``` +``` +Complete visibility +for compliance. +System +(Automation +Layer) +``` +``` +Consolidates scores, generates AI summaries, and +logs actions for audit. +``` +``` +Background +execution. +``` +### 6.11 Interview Feedback & Evaluation Summary + +**6.11.1 Functionality Scope** + +The **Interview Feedback & Evaluation Summary** module consolidates all interview-level +assessments, feedback remarks, and scoring for each applicant in the dealership onboarding +process. +It provides a transparent, structured, and comparable view of candidate evaluations across levels, +helping decision-makers validate suitability based on quantified scores, qualitative remarks, and +panel feedback. + + +**6.11.2 Width** + +- Accessible under the **Interviews tab** within the Application Detail View. +- Displays interview data level-wise, including: + o **Interviewer Name and Role** + o **Individual Scores** (out of configured weightage) + o **Evaluator Remarks** and **Feedback Summary** + o **Level-wise Overall Assessment and Decision Status** +- Supports multiple interview rounds such as **Level 1 (DD-ZM / RBM)** , **Level 2 (ZBH / DD** + **Lead)** , and **Level 3 (DD Head / NBH)**. + +**6.11.3 Depth** + +``` +6.11.3.1 Interview Recording & Display +``` +- Each panel member records their evaluation through structured scoring criteria linked to + the **KT Matrix** +- **KT Matrix (Kepner Tregoe Matrix)** is used to assess structured decision parameters + during evaluation. +- The KT Matrix auto-calculates weighted scores based on parameters such as: + o Business Acumen + o Market Understanding + o Financial Readiness + o Passion for the Brand + o Leadership and Team Capability +- Individual evaluator entries capture: + o **Interviewer Name & Designation (Role)** + o **Score / Weightage** + o **Remarks** (qualitative observation) + o **Feedback Summary** (behavioral and communication assessment) +- Scores from all panelists are auto-averaged to display the **Level Total Score** and **Rank** for + each candidate. + +``` +6.11.3.2 Level-Wise Summaries +``` +- Each interview level concludes with a **Level Summary** section containing: + o **Decision Status:** _Approved / Rejected / Hold_ + o **Approver Comments:** Automatically tagged with evaluator roles (e.g., โ€œApproved + by both ZBH and DD Leadโ€). + o **Overall Assessment:** Concise narrative summarizing candidate strengths, + e.g., _โ€œStrong candidate with excellent business plan and clarity of thought.โ€_ + + +- This ensures consistent evaluation format and avoids subjective or incomplete data + entries. + +``` +6.11.3.3 Data Traceability & Access Control +``` +- Each interview and feedback entry is timestamped and recorded in the **Audit Trail** for + compliance. +- Panel members can only view their respective entries until the stage closes. +- Once finalized, the complete evaluation summary becomes visible to higher authorities + (DD-Head, NBH) for reference during final selection. +- No feedback modification is allowed post submission to preserve data integrity. + +**6.11.4 Personas-Wise Accessibility & Visibility** + +``` +Persona Accessibility Visibility Scope +Interview Panel (DD-ZM / +RBM / ZBH / DD Lead / DD +Head / NBH) +``` +``` +Can record scores, remarks, and feedback +for assigned levels. +``` +``` +Access limited to +their interview +stage. +DD-Admin Can view all level-wise feedback and +compile summaries for reporting. +``` +``` +Full visibility and +export control. +DD-Head / NBH Can review all interview levels, +aggregated scoring, and AI +recommendations before final approval. +``` +``` +Read-only visibility +at summary stage. +``` +``` +Applicant / Dealer Prospect No access to internal evaluation data. Not visible. +System (Automation Layer) Aggregates scores, computes averages, +and stores all evaluation logs for audit +traceability. +``` +``` +Background +operation. +``` +### 6.12 Application Approval & Rejection Workflow + + +**6.12.1 1. Functionality Scope** + +The **Application Approval & Rejection Workflow** manages structured decision-making at each +level of the dealer onboarding process. It enables authorized evaluators and interview panel +members to **approve or reject** dealership applications with mandatory remarks and optional +attachments, ensuring transparent and traceable decisions. This feature operates throughout all +workflow stages โ€” from **Level 1 to Level 3** โ€” and captures evaluation outcomes in a unified +format that becomes part of the applicantโ€™s permanent record. Each action taken is time- +stamped, logged, and visible to subsequent reviewers, promoting accountability across the +approval chain. + +**6.12.2 Width** + +- Integrated into the **Application Detail View** and accessible to all reviewers participating + in the **approval hierarchy**. +- The feature appears as an **action modal** during each evaluation stage, allowing the panel + to record feedback through one of two options: + o **Approve Application** โ€“ with required remarks and optional document upload. + o **Reject Application** โ€“ with mandatory justification for rejection. +- Available at all major decision levels: + o **Level 1:** DD-ZM + RBM + o **Level 2:** DD-Lead + ZBH + o **Level 3:** NBH + DD-Head +- Each levelโ€™s action (approve or reject) is visible in the **Application Progress** + **Tracker** and **Audit Trail**. + +**6.12.3 Depth** + +- **Approval Action:** + o The panelist provides a **mandatory remark** summarizing the rationale behind + approval. + o Supporting documents, if any (e.g., business justification, property proof, or + presentation decks), can be attached optionally. + o Upon submission, the system updates the application status (e.g., _Level 1_ + _Approved, Level 2 Approved_ ) and logs the decision details for audit tracking. + o It Also triggers the application to subsequent next level +- **Rejection Action:** + o The panelist provides a **mandatory reason for rejection** , clearly outlining the + grounds for disqualification. + o The system changes the status to _Rejected_ and notifies the applicant and + previous-level reviewers via email and WhatsApp. + + +``` +o Rejected applications are archived for reference and may be reopened only +through Admin authorization. +``` +- **Feedback Integration:** + o Each levelโ€™s panel (DD-ZM, RBM, DD-Lead, ZBH, NBH, DD-Head) must record their + respective feedback before submitting approval or rejection. + o The recorded remarks automatically feed into the **consolidated interview and** + **feedback record** , used for AI-assisted summary generation at the NBH stage. +- **Audit & Traceability:** + o Every approval or rejection entry is **time-stamped** with user ID and role. + o The system maintains a **complete audit trail** showing who approved, rejected, or + commented, along with corresponding remarks and uploaded documents. + o This ensures transparent review continuity across all approval levels. + +**6.12.4 Personas-Wise Accessibility & Visibility** + +``` +Persona Accessibility Visibility Scope +DD-ZM / RBM +(Level 1) +``` +``` +Approve or reject applications post-KT Matrix +evaluation. +``` +``` +Region-specific +visibility. +DD-Lead / ZBH +(Level 2) +``` +``` +Review Level 1 feedback, provide comments, and +approve or reject accordingly. +``` +``` +Zone-level visibility. +``` +``` +NBH / DD-Head +(Level 3) +``` +``` +Final approval or rejection with AI-assisted +recommendation review. +``` +``` +Complete visibility. +``` +``` +DD-Admin Monitor decision trail, manage reassignments, and +maintain approval integrity. +``` +``` +Full administrative +visibility. +System +(Automation +Layer) +``` +``` +Updates application status, logs actions, and +triggers notifications via email & whatsapp to +applicant +``` +``` +Background +operation. +``` + +### 6.13 Work Notes & Internal Communication Trail + +**6.13.1 Functionality Scope** + +The **Work Notes & Internal Communication Trail** module serves as the centralized collaboration +channel for each dealership application. It enables authorized Royal Enfield stakeholders to +record, track, and exchange contextual comments directly within the system, eliminating the +need for external emails or offline communication. + +Each work note is linked to a specific application, allowing panel members and reviewers to +maintain a continuous, transparent record of discussions and decisions. The feature improves +traceability, facilitates faster internal communication, and ensures that every remark is +permanently associated with its respective application record. + +Work Notes serve as the **official communication channel for Send Back actions** , capturing +reviewer remarks and notifying concerned users within the resignation workflow. Work Notes +act as the **official communication channel for all Send Back and Revoke actions** across the +resignation workflow. + + +**6.13.2 Width** + +- Accessible under **Application Detail View โ†’ Work Notes** tab. +- Available to all internal users participating in the onboarding and approval workflow (DD- + ZM, RBM, DD-Lead, ZBH, DD-Head, NBH, Finance, and Legal). +- Displays a **chronological thread** of messages, with each entry showing the **comment** + **author, role, timestamp, and tagged participants**. +- Allows cross-functional interaction and tagging for efficient information exchange and + resolution tracking. + +**6.13.3 Depth** + +- **Comment Logging:** + o Users can post comments, share clarifications, or document updates related to an + application. + o Each message is stored under the respective application ID to preserve discussion + context. +- **Tagging & Notifications:** + o Authorized users can **tag other stakeholders** using the โ€œ@mentionโ€ feature to + seek inputs or actions. + o Tagged users receive **email notifications** and **system alerts** with a direct link to + the corresponding work note. +- **Visibility & Access Control:** + o All internal users (RE employees) involved in the workflow can view the entire + communication thread for the application. + o **FDD (Financial Due Diligence) users** , being external partners, can **add comments** + **or upload clarifications** related to their scope of review but **cannot view** + **comments made by other users**. + o This restricted access ensures confidentiality of internal deliberations while still + allowing FDD to communicate and provide input efficiently. +- **Integration & Traceability:** + o Work Notes are linked with system actions such as **interview** + **scheduling** , **approvals** , and **feedback submissions** for contextual reference. + o So every activity will also be logged in work note as well. + o Every note is **timestamped** and logged under the **Audit Trail** for compliance + verification. + +**6.13.4 Personas-Wise Accessibility & Visibility** + +``` +Persona Accessibility Visibility Scope +``` + +#### DD-ZM / RBM / DD- + +``` +Lead / ZBH / NBH / DD- +Head +``` +``` +Can post, tag users, and view all +comments related to the application. +``` +``` +Full visibility within +their assigned region or +zone. +DD-Admin Can monitor all communication threads, +ensure comment quality, and flag +unresolved discussions. +``` +``` +Complete visibility. +``` +``` +Finance / Legal Can view and contribute to discussions +when tagged for specific clarifications. +``` +``` +Tag-based visibility. +``` +``` +FDD (External Agency) Can post comments and attach files +related to financial review but cannot +view othersโ€™ remarks. +``` +``` +Restricted visibility โ€“ +view only own +comments. +System (Automation +Layer) +``` +``` +Sends notifications for tags, logs all +messages, and maintains chronological +order in the Audit Trail. +``` +``` +Background operation. +``` +### 6.14 System Notifications & Alerts + +**6.14.1 Functionality Scope** + +The **System Notifications & Alerts** module ensures timely communication of important events +and workflow updates to all authorized users involved in the dealer onboarding and offboarding +process. It serves as an in-application and email-based alert mechanism that informs users about +key actions such as application assignments, interview scheduling, document verification, and +status updates. + +**6.14.2 Width** + +- Accessible from the **top navigation bar** under the bell icon in the application interface. + + +- Displays a **dropdown list of recent notifications** , each showing: + o A concise description of the event (e.g., _โ€œInterview scheduled for APP-001โ€_ ). + o The timestamp indicating when the event occurred. + o A visual indicator for unread alerts. +- Includes a **โ€œView All Notificationsโ€** option that redirects users to the complete + notification history page. +- Notifications are automatically generated for workflow events such as: + o New application assignment + o Interview scheduling or rescheduling + o Document verification completion + o Feedback submission + o Application approval or rejection + o Comment tagging in Work Notes + +**6.14.3 Depth** + +- **Trigger Logic:** + o Notifications are auto-triggered by specific actions performed within the system, + such as approval submission, interview creation, or applicant tagging. + o Each alert is linked to the corresponding application ID, ensuring contextual + reference when accessed. +- **Delivery Channels:** + o Notifications appear as **in-system pop-ups** and are also stored under the + notification dropdown. + o For critical actions (e.g., interview scheduling or application assignment), an **email** + **alert** is also sent to the concerned user. + o For critical actions (e.g., interview scheduling, application assignment, or pending + user actions), alerts are **sent via email and through WhatsApp**. +- **Read & Unread Management:** + o Unread notifications are highlighted with an indicator dot until opened. + o Once viewed, alerts are marked as read and retained in the userโ€™s notification + history for reference. +- **Audit Traceability:** + o All notifications are logged under the **Audit Trail** , maintaining a traceable record + of all communication triggers and delivery timestamps. + +**6.14.4 Personas-Wise Accessibility & Visibility** + +``` +Persona Accessibility Visibility Scope +DD-ZM / RBM / DD- +Lead / ZBH / DD-Head / +NBH +``` +``` +Receive workflow alerts (assignments, +interviews, document verification, feedback). +``` +``` +Role-specific +notifications. +``` + +``` +DD-Admin Can view and manage all notifications +generated across modules for monitoring +purposes. +``` +``` +Full visibility. +``` +``` +Finance / Legal / FDD Receive notifications related to their assigned +applications or document validation events. +``` +``` +Restricted to tagged +or assigned cases. +Applicant / Dealer +Prospect +``` +``` +Receive notifications for interview schedules, +approvals, feedback outcomes, and pending +actions via email and WhatsApp. +``` +``` +External limited +scope. +``` +``` +System (Automation +Layer) +``` +``` +Generates, delivers, and logs notifications +with timestamps. +``` +``` +Background +operation. +``` +### 6.15 FDD (Financial Due Diligence) & Finance Module + +**6.15.1 Functionality Scope** + +The **FDD & Finance Module** manages the complete **Financial Due Diligence (FDD)** and +subsequent **Finance Review** workflow for dealership onboarding. It enables external FDD +partners to securely access their assigned applications, upload financial evaluation reports, and +collaborate with internal stakeholders through integrated Work Notes. +The module ensures **restricted access, secure data handling, and traceable financial review** , +providing a seamless interface for Royal Enfieldโ€™s internal teams and external agencies to +collaborate while maintaining compliance and confidentiality. + + +**6.15.2 Width** + +- Accessible through **RE SSO credentials** created for authorized external FDD partners. +- Once logged in, FDD users can view **only the applications assigned to them** for financial + due diligence. +- Available features for the FDD role include: + o **Limited Application View** โ€” displaying key applicant details necessary for + financial review. + o **Document Upload Interface** โ€” to submit financial artefacts and reports. + o **Work Notes Section** โ€” to raise queries or communicate updates with the RE + Admin team. + o **Submit Report Action** โ€” for finalizing FDD evaluation. +- For internal users (Finance Team), this module extends to review, validate, and finalize + financial recommendations post-FDD submission. + +**6.15.3 Depth** + +``` +6.15.3.1 FDD Workflow & Capabilities +``` +- **Access & Authentication:** + o Each FDD partner receives a **dedicated SSO login** configured through REโ€™s identity + system, ensuring secure and auditable access. + o Upon login, the FDD user dashboard displays **only assigned applications** marked + for due diligence. + o External users have **limited access** โ€” they cannot view internal remarks, + evaluations, or other applications outside their assignment. +- **Application View & Interaction:** + o FDD users can access restricted details such as applicant name, business location, + and required financial documents. + o They can upload their findings under the **Documents Section** and communicate + updates or issues through **Work Notes**. + o The Work Notes act as a **query and escalation tool** โ€” allowing FDD users to + request missing documents, seek clarifications, or flag non-responsive applicants. +- **Work Notes Integration:** + o FDD users can add notes tagged to **DD-Admin** or **Finance Team** for specific actions. + o In case the applicant fails to respond or provide requested documents, the FDD + user adds a note citing **โ€œNon-responsiveness from applicantโ€** and **returns the** + **application** to Admin for closure or reallocation. + o All such notes are logged chronologically with timestamps and author details for + compliance. +- **Document Upload & Submission:** + + +``` +o FDD users upload essential financial documents and reports such as: +โ–ช Bank Statements +โ–ช Income Tax Returns +โ–ช Credit Reports +โ–ช Property Papers +โ–ช Business Valuation Reports +o Each file entry records: +โ–ช File Name and Type +โ–ช Upload Date and Time +โ–ช Uploaded By (User ID / Role) +o Once the report is complete, the FDD user marks it as Submitted , which locks +further edits. +``` +- **Finance Team Review:** + o After submission, the **Finance Team** reviews the FDD report and uploaded + artefacts. + o They evaluate whether the applicant qualifies financially to move forward in the + onboarding process. + o Finance team members may also log remarks, flag discrepancies, or mark an + application as **โ€œApproved for Next Stageโ€** or **โ€œRejected on Financial Grounds.โ€** + o Their decision updates the **Application Journey Tracker** and notifies **DD-** + **Admin** and **NBH**. +- **Confidentiality & Audit Compliance:** + o FDD users cannot view internal evaluations, interview feedback, or progress notes + beyond their assigned stage. + o All uploads, submissions, and Work Note interactions are **timestamped** and + recorded under the **Audit Trail**. + o The Finance Teamโ€™s review decisions are also logged for traceability and reporting. + +**6.15.4 Personas-Wise Accessibility & Visibility** + +``` +Persona Accessibility Visibility Scope +FDD Team +(External Partner) +``` +``` +Can log in via SSO, view assigned +applications only, upload documents, and +communicate via Work Notes. +``` +``` +Restricted to assigned FDD +stage and specific +applications. +DD-Admin Can assign applications to FDD users, +monitor progress, and review Work Notes +or returned cases. +``` +``` +Full visibility across all +applications. +``` +``` +Finance Team Reviews submitted FDD reports, validates +financial compliance, and approves or +rejects based on findings. +``` +``` +Complete visibility of all +financial artefacts and +remarks. +DD-Head / NBH View finance-approved FDD reports for final +validation before LOI approval. +``` +``` +Read-only visibility post- +finance review. +``` + +``` +System +(Automation +Layer) +``` +``` +Controls access through SSO, logs all +interactions, updates status, and notifies +stakeholders. +``` +``` +Background operation. +``` +### 6.16 LOI Approval & Issuance + +**6.16.1 Functionality Scope** + +The **LOI Approval & Issuance** module governs the structured process of validating, approving, +and issuing the **Letter of Intent (LOI)** once the dealer applicant successfully clears all financial and +operational evaluations. It ensures that every LOI is issued only after submission and verification +of mandatory documents, confirmation of the security deposit, and formal approval by +authorized stakeholders โ€” **Finance, DD-Head, and NBH**. This module brings +complete **transparency, document-level traceability, and compliance integrity** , ensuring that no +dealership appointment progresses without validated and approved documentation. + +**6.16.2 Width** + +- Accessible under **Application Journey โ†’ LOI Approval / LOI Issue** stages. +- Used sequentially by **DD-Admin** , **Finance** , **DD-Head** , and **NBH**. +- Major functional components include: + o **LOI Document Request** + o **Security Deposit Confirmation** + o **Approval Chain (Finance โ†’ DD-Head โ†’ NBH)** + o **Final LOI Preparation and Issuance** + + +- Once approved, the LOI is issued and the application progresses automatically to **Security** + **Details** and **Dealer Code Generation** stages. + +**6.16.3 Depth** + +``` +6.16.3.1 Document Request & Collection +``` +- Upon completion of the final interview and financial approval, the **DD-Admin** triggers an + automated **LOI Document Request** to the applicant. +- The applicant is required to upload the prescribed set of documents and artefacts in + a **Linked Folder** , following the official naming convention: + o **Region โ†’ Name of Prospect โ†’ Location โ†’ Interview Date โ†’ LOI Issuance Date.** +- The folder and files are reviewed by the Admin and Finance teams for completeness + before progressing to approval. + +``` +6.16.3.2 Mandatory LOI Document Checklist +``` +The following artefacts must be submitted before the LOI can be approved: + +- DIP Booklet โ€“ filled and signed by RBM +- Profile Sheet +- Dealership Application Form +- Interview Feedback Forms (RBM and ZBH) +- Land Selection Criteria Sheet +- Logic Note and Comparative Logic Note +- Zonal Evaluation Form +- Authorization Letter +- City Map (PPT) +- Proposed Location Photos (minimum 20, PPT) +- Layout Drawings (PPT) +- Viability Sheet +- Project Plan +- Self-signed PAN/Aadhaar of all partners (both sides) +- CIBIL Reports of all partners +- Dealership Name & Address Email from RBM +- Rental / Lease Agreement or Consent Letter from Landlord +- Security Deposit Proof (to be uploaded **only after** document set completion) + + +``` +6.16.3.3 Folder Verification & Tracking +``` +- The Admin verifies that all files are uploaded in the specified folder format and uses + metadata such as **Interview Date** , **LOI Issuance Date** , and **Document Ageing** (days + between interview and issuance) to track process efficiency. +- Any missing or incorrect artefacts trigger a system reminder to the applicant or DD-Admin + for rectification before the approval process can begin. + +``` +6.16.3.4 Security Deposit Validation +``` +- After successful folder verification, the **Admin requests the applicant to perform the** + **security deposit transfer via RTGS**. +- Deposit proof (transaction slip or confirmation) is uploaded into the folder and validated + by Finance. +- Only after deposit confirmation does the system allow LOI preparation to proceed. + +``` +6.16.3.5 Approval Workflow +``` +- The **LOI document** is generated using the approved RE format and automatically + populated with applicant and dealership details. +- The approval routing follows this sequence: + o **Finance Team:** Reviews document completeness and verifies the security deposit. + o **DD-Head:** Validates business justification and network alignment. + o **NBH:** Provides final release authorization through the system. +- Each approver records mandatory remarks before submission, ensuring transparency at + every step. +- Once NBH approves, the LOI is marked as **โ€œReady to Issue.โ€** + +``` +6.16.3.6 LOI Issuance +``` +- **DD-Admin** uploads the final signed LOI under the **LOI Issue** stage. +- The system triggers: + o An **official email notification** with the issued LOI is sent to the dealer upon + completion of the LOI issuance stage. This will **not be sent on WhatsApp.** + o Alerts to **Finance** , **DD-Head** , and **NBH** confirming completion of issuance. +- The applicant must upload an **LOI Acknowledgement Copy** with seal and signature to + confirm receipt. + +``` +6.16.3.7 Document & Artefact Tracking +``` +- Every LOI-related artefact includes: + o File Name and Type + + +``` +o Uploaded By (User Role and Name) +o Upload Timestamp +o Version (Draft, Final, Signed Copy) +o Download Link +``` +- Each file upload, review, and replacement is logged under the **Audit Trail** to maintain a + chronological record of actions. + +**6.16.4 Personas-Wise Accessibility & Visibility** + +``` +Persona Accessibility Visibility Scope +DD-Admin Initiates document requests, validates uploads, +manages security deposit confirmation, and +uploads final LOI. +DD ASM, DD ZM, and RBM shall have view-only +access to the workflow. +The DD Lead shall have approval visibility , without +direct approval authority at this stage. +``` +``` +Full access and edit +rights. +``` +``` +Finance Team Verifies security deposit and financial artefacts +before LOI preparation. +``` +``` +Full visibility for +validation. +DD-Head Approves LOI content and validates alignment with +dealership network plans. +``` +``` +Approval-level +visibility. +NBH Provides final release authorization and approves +LOI issuance. +``` +``` +Full visibility for +sign-off. +Applicant / Dealer +Prospect +``` +``` +Uploads required LOI documents and provides +security deposit confirmation. +``` +``` +Access limited to +own uploads. +System +(Automation +Layer) +``` +``` +Monitors document checklist, logs folder actions, +routes approvals, calculates ageing, and triggers +notifications. +``` +``` +Background +operation. +``` +### 6.17 Dealer Code Generation, Architectural Work & Statutory Documentation............ + + +**6.17.1 Functionality Scope** + +This consolidated module covers the post-LOI implementation stages that transition a selected +dealer from approval to operational readiness. It manages three critical workflows: + +- **Dealer Code Generation** โ€“ creation of a unique, system-integrated dealership identifier. +- **Architectural Work** โ€“ coordination between Admin and Brand Experience teams to + execute layout, design, and site readiness. +- **Statutory Documentation** โ€“ collection and validation of mandatory compliance + documents. + +Together, these processes ensure the dealership becomes fully compliant, aligned with Royal +Enfieldโ€™s brand standards, and ready for inauguration. + +**6.17.2 Width** + +- Accessible sequentially in the **Application Journey** after _LOI Issued_. +- Managed by **DD-Admin** , with participation from **Architecture / Brand Experience**. +- Progress and status for each sub-module are reflected in the **Progress Tracker**. + +**6.17.3 Depth** + +``` +6.17.3.1 A. Dealer Code Generation +``` +- Once the LOI is acknowledged, **DD-Admin** initiates **dealer code creation** through SAP + using an **OData API integration**. +- The system generates and stores multiple associated codes for: + o **Sales Code** + o **Service Code** + o **Genuine Motorcycle Accessories (GMA) Code** + o **Gear Code** +- These codes uniquely identify all dealer operations across RE systems (DMS, MSD, CRM). +- Code creation details (initiator, timestamp, and reference IDs) are recorded in the **Audit** + **Trail**. +- The application status updates automatically to _Dealer Code Generated_ and triggers + notifications to DD-Admin, Finance, and Legal. + +``` +6.17.3.2 Architectural Work (Brand Experience Team) +``` +- After code generation, **DD-Admin assigns the case** to the **Architecture / Brand** + **Experience Team** for site design and infrastructure execution. + + +- The workflow covers: + o **Part A โ€“ Architecture:** + โ–ช Admin assigns case โ†’ Architecture Team. + โ–ช Architecture uploads **DWG layout** and **site dimension drawings**. + โ–ช Dealer provides written consent via email confirming acceptance of layout + as per Vastu and design guidelines. + โ–ช Final layout issued to dealer along with **multiple drawing sets** for + construction reference. + โ–ช Dealer initiates infrastructure work; progress tracked through uploaded + photographs or reports. + o **Part B โ€“ Statutory Fulfilment:** + โ–ช Admin and Architecture teams collect mandatory statutory compliance + documents from the dealer. + โ–ช These include government, financial, and brand compliance artefacts + required before EOR. +- The Architecture team updates milestone completion, and all uploads (drawings, + approvals, and dealer confirmations) are recorded with timestamps and uploader details. + +``` +6.17.3.3 Statutory Documentation +``` +- At this stage, **DD-Admin** collects and verifies all statutory and regulatory documents + necessary for legal and operational readiness. +- The standard document checklist includes: + o GST Registration Certificate + o PAN + o Nodal Agreement + o Cancelled Cheque + o Partnership Deed / LLP / MOA / AOA / COI + o Firm Registration Certificate + o Rental / Lease / Land Agreement + o Virtual Code Confirmation + o Domain ID for _@dealer.royalenfield.com_ + o MSD (Microsoft Dynamics) Configuration confirmation (ledger setup) + o LOI Acknowledgement Copy (signed by dealer) +- Each document record displays: + o File name and type + o Uploaded by (user role and name) + o Upload timestamp + o Version (if replaced or re-uploaded) +- Files are viewable and downloadable from within the application, and all versions are + retained for compliance audits. + + +- The **Finance and Legal teams** verify documents, update status to _Verified / Pending / Re-_ + _submit Required_ , and add remarks. +- Once all statutory items are verified, the system automatically transitions the application + to the **EOR (Essential Operating Requirements)** stage. + +**6.17.4 Personas-Wise Accessibility & Visibility** + +``` +Persona Accessibility Visibility Scope +DD-Admin Initiates dealer code creation, coordinates +architectural assignments, and collects +statutory documents. +DD ASM, DD ZM, and RBM shall have view- +only access for monitoring and reference +purposes. +``` +``` +Full visibility and +control. +``` +``` +Architecture / Brand +Experience Team +``` +``` +Uploads drawings, issues layout approvals, +and updates site readiness milestones. +``` +``` +Limited to assigned +applications. +Finance Team Reviews statutory artefacts (GST, banking, +MSD) and confirms financial readiness. +``` +``` +Verification-level +visibility. +Legal Team Verifies agreements, firm registration, and +compliance documents. +``` +``` +Tag-based visibility +for assigned items. +Dealer / Applicant Uploads statutory artefacts and provides +consent on architecture layouts. +``` +``` +Restricted to own +uploads. +System (Automation +Layer) +``` +``` +Syncs SAP dealer code generation, updates +stage transitions, and logs all artefacts with +timestamps. +``` +``` +Background +operation. +``` +### 6.18 LOA Issuance, Essential Operating Requirements & Inauguration + +**6.18.1 Functionality Scope** + +The **LOA Issuance, Essential Operating Requirements (EOR) & Inauguration** module captures +the final execution phase of the dealer onboarding lifecycle. It ensures that only those dealerships +which have fulfilled all architectural, statutory, and financial prerequisites are authorized to +commence operations under Royal Enfieldโ€™s network. +This module manages the formal **Letter of Authorization (LOA)** release, verification of **EOR +compliance** , and the **dealership inauguration process** , providing complete visibility, audit +control, and cross-departmental coordination before official go-live. + +The **Letter of Authorization (LOA) is a parallel statutory activity** and is **not dependent on +infrastructure readiness** for issuance. LOA processing and issuance proceed **in parallel with** + + +**statutory compliance checks** , while infrastructure readiness is tracked independently for final +go-live. + +**6.18.2 Width** + +- Accessible sequentially in the **Application Journey** , following completion of statutory + compliance and architectural validation. +- Managed primarily by **DD-Admin** , with review and approvals by **DD-** + **Head** , **NBH** , **Architecture** , **Training** , and **Brand Experience** teams. +- Tracks readiness status and documents through EOR and Inauguration milestones. + +**6.18.3 Depth** + +``` +6.18.3.1 LOA (Letter of Authorization) Issuance +``` +- Once statutory verification and site readiness are complete, **DD-Admin** initiates the **LOA** + **document preparation**. +- The **LOA** serves as Royal Enfieldโ€™s formal authorization, confirming that the dealer has + met all required pre-operational standards. +- The approval routing follows: + o **DD-Head:** Reviews infrastructure completion, EOR readiness, and compliance + artefacts. + o **NBH:** Grants final sign-off authorizing the dealership to operate. +- The finalized LOA document is uploaded into the system by DD-Admin, tagged with: + o Issue Date + o Authorized Signatory (DD-Head / NBH) + o Document Version and Upload Timestamp +- The LOA issuance automatically updates the Application Journey status to _โ€œAuthorized for_ + _Operations.โ€_ +- System-generated notifications are sent to all relevant teams, confirming dealership + activation. + +``` +6.18.3.2 Essential Operating Requirements (EOR) +``` +- The **EOR checklist** ensures that each new dealership is fully operationally ready prior to + launch. +- It includes mandatory pre-opening parameters across business, facility, and IT domains, + such as: + o Display Vehicle Readiness + + +``` +o Brand Signage Installation +o Training Completion for Sales and Service Teams +o MSD / DMS Configuration and Connectivity +o Availability of Service Tools, Equipment, and Spare Inventory +o Safety, Security, and Facility Compliance Checks +o Test Ride Vehicles and Customer Experience Readiness +``` +- Each EOR line item is verified and marked as _Complete / Pending / Non-Compliant_ by the + respective functional team (Architecture, Training, IT, Service). +- The system records: + o EOR Checklist File + o Verification Date + o Verified By (User and Role) + o Comments or Exception Notes +- Once all checklist parameters are marked as _Complete_ , DD-Admin updates the EOR stage + status to _EOR Completed_. +- This completion enables scheduling of the final **Inauguration**. + +``` +6.18.3.3 Inauguration & Go-Live +``` +- Upon EOR completion, **DD-Admin** coordinates with the **NBH, ZBH, RBM** , and **Brand** + **Experience** teams to finalize the inauguration plan. +- The **inauguration details** are logged in the system, including: + o Inauguration Date and Venue + o Attendees (NBH, DD-Head, ZBH, RBM, Architecture, Brand Experience) + o Photographs and Event Summary Report +- Post-event, the Admin uploads the **Inauguration Report** and related media (photographs, + press releases, or video references). +- The system marks the application as _โ€œDealership Live / Onboarded.โ€_ +- Key metadata such as inauguration date, event photos, and final status are stored under + the **Application Journey** for historical tracking. + +**6.18.4 Personas-Wise Accessibility & Visibility** + +``` +Persona Accessibility Visibility Scope +DD-Admin Manages LOA creation, tracks EOR checklist +completion, and logs inauguration details. +The DD ASM is authorized to upload LOA- +related documents to support statutory and +compliance verification +``` +``` +Complete visibility +and control. +``` +``` +DD-Head / NBH Approve LOA issuance and review final +readiness for operational authorization. +``` +``` +Approval-level +visibility. +``` + +``` +Architecture / +Brand Experience +``` +``` +Verify physical readiness, signage, and brand +compliance. +``` +``` +Access limited to +assigned EOR items. +Training / IT / +Service Teams +``` +``` +Verify operational readiness across staff +training, tools, and systems configuration. +``` +``` +Tag-based visibility. +``` +``` +Dealer / Applicant Acknowledges LOA receipt and supports +inauguration planning. +``` +``` +Restricted to their +assigned application. +System +(Automation Layer) +``` +``` +Logs EOR verifications, uploads inauguration +metadata, triggers status updates and +notifications. +``` +``` +Background +operation. +``` +### 6.19 Essential Operating Requirements (EOR) Checklist + +**6.19.1 Functionality Scope** + +The **Essential Operating Requirements (EOR) Checklist** module ensures that all pre-launch +business, infrastructure, compliance, and operational prerequisites are fulfilled before a +dealership is formally inaugurated. + +**6.19.2 Width** + +- Accessible under the **EOR Checklist tab** in the Application Detail View. +- Displays a checklist of all operational readiness parameters with their current completion + status. +- Each item includes: + o Parameter Name (e.g., _Sales Standards, DMS Infra, Manpower Training_ ) + + +``` +o Status Indicator ( Pending / Completed / Verified ) +o Assigned Team or Reviewer (Architecture, IT, Finance, Training, etc.) +``` +- The progress bar at the top dynamically updates the **EOR completion percentage** based + on verified items. +- Supports both in-system verification and document-based validation uploads. + +**6.19.3 Depth** + +``` +6.19.3.1 EOR Parameter Configuration +``` +- The checklist is pre-configured with mandatory items applicable to every dealership + before activation. + +``` +6.19.3.2 Status Management & Verification +``` +- Each item can be marked as _Pending_ , _In Progress_ , or _Completed_ by the respective + responsible department. +- Functional owners (Finance, Training, IT, etc.) verify and mark their sections complete. +- The **DD-Admin** oversees all updates, ensuring every checklist item is validated before + final approval. +- Completion of all mandatory parameters automatically updates the stage to _EOR_ + _Completed_. +- Remarks or proof of completion (documents, screenshots, or photos) can be attached to + each item for audit purposes. + +``` +6.19.3.3 Progress Calculation & Tracking +``` +- The system automatically calculates the EOR completion percentage based on total + verified parameters. +- Visual progress indicators are shown both at item and overall level. +- Hover or click actions reveal who completed each parameter and when. +- The **Audit Trail** logs each checklist update with timestamps for traceability. + +``` +6.19.3.4 Integration with Inauguration Readiness +``` +- Once the EOR checklist reaches 100% completion, the system triggers a readiness alert + to **DD-Head** and **NBH**. +- The application automatically transitions to the **Inauguration Stage** , initiating event + scheduling and brand readiness verification. +- EOR verification data remains accessible for post-launch audits. + + +**6.19.4 Personas-Wise Accessibility & Visibility** + +``` +Persona Accessibility Visibility Scope +DD-Admin Monitors and updates EOR checklist progress, +consolidates remarks, and verifies stage +completion. +DD ASM and DD ZM are authorized to upload +site readiness details. +DD Lead, RBM, and ZBH shall have view-only +access for review and governance. +``` +``` +Full visibility and +edit rights. +``` +``` +Architecture / Brand +Experience +``` +``` +Update and mark parameters complete as per +their domain. +``` +``` +Tag-based +restricted visibility. +DD-Head / NBH View final EOR completion status and remarks +before authorizing inauguration. +``` +``` +Read-only visibility +for approval. +Dealer / Applicant May be asked to upload supporting artefacts +(photos, certificates, invoices). +``` +``` +Limited to assigned +items. +System (Automation +Layer) +``` +``` +Calculates completion percentage, updates +stage transitions, and logs verification activities. +``` +``` +Background +operation. +``` +### 6.20 Progress Tracker....................................................................................................... + + +**6.20.1 Functionality Scope** + +The **Application Journey & Progress Tracker** provides a complete visual representation of the +dealership applicationโ€™s lifecycle โ€” from submission through multi-level evaluations, due +diligence, and final dealership inauguration. It will be maintained in itemized way for each +applicant +It allows all authorized Royal Enfield users to track each milestone in real time, review supporting +documents, and monitor who performed specific actions and when. This ensures **end-to-end +transparency, document-level traceability, and operational accountability** throughout the +dealer onboarding and approval process. + +**6.20.2 Width** + +- +- Accessible within **Application Detail View โ†’ Progress Tab**. +- Displays a **vertical, stage-based timeline** of all workflow steps with corresponding + completion statuses. +- Each milestone includes: + o Stage title and brief description (e.g., _1st Level Interview โ€“ DD-ZM + RBM_ + _Evaluation_ ). + o Evaluator details and assigned roles. + o Status indicator ( _Completed, In Progress, Pending_ ). + o Date and timestamp of completion. + o **Document and artefact count** , with clickable links to download or review the + uploaded files. +- Covers all workflow phases โ€” from **application submission** to **inauguration and go-live**. +- LOI Issue โ†’ Dealer Code Generation โ†’ **LOA Issuance** โ†’ **EOR Checklist Completion** โ†’ + Inauguration & Go-Live + +**6.20.3 Depth** + +``` +6.20.3.1 Stage-Wise Progress Representation: +``` +``` +o Submitted: Application logged with basic details and initial document uploads. +o Questionnaire: Applicant questionnaire completed and scored. +o Shortlist: DD-Admin review with uploaded validation documents. +o 1st Level Interview: Conducted by DD-ZM and RBM , with evaluator remarks and +attachments. +o 2nd Level Interview: Conducted by DD-Lead and ZBH , capturing evaluation +documents and recommendations. +``` + +``` +o 3rd Level Interview: Conducted by NBH and DD-Head , includes final approval +documentation and AI recommendation summary. +o FDD (Financial Due Diligence): Handled by the FDD partner for financial validation; +documents uploaded for review. +o LOI Approval: Preparation and verification for Letter of Intent issuance. +o Security Details: Security and compliance document verification stage. +o LOI Issue: Formal Letter of Intent generated and uploaded. +o Dealer Code Generation : Dealer Code creation is initiated upon trigger by the DD- +Admin , created in the SAP Master , and logged against the application for audit +and traceability. +o Architectural Work: Contains sub-steps โ€” +โ–ช Assigned to Architecture Team +โ–ช Architectural Document Upload +โ–ช Architecture Team Completion +o Statutory Documents: Eleven-step checklist for compliance uploads including: +โ–ช GST Certificate +โ–ช PAN +โ–ช Nodal Agreement +โ–ช Cancelled Cheque +โ–ช Partnership Deed / LLP / MOA / AOA / COI +โ–ช Firm Registration Certificate +โ–ช Rental / Lease / Land Agreement +โ–ช Virtual Code +โ–ช Domain ID +โ–ช MSD Configuration +โ–ช LOI Acknowledgement Copy +o LOA (Letter of Authorization): Issued after LOI acceptance. +o EOR (Essential Operating Requirements): Verification of pre-opening operational +criteria. +o Inauguration: Final dealership launch milestone. +``` +**6.20.3.2** Document & Artefact Management: + +``` +o At every stage, the tracker displays document and artefact names , along +with downloadable links for review. +o Each document entry includes: +โ–ช File name and type (PDF, image, Excel, etc.) +โ–ช Uploaded by (user name and designation) +โ–ช Upload timestamp and workflow stage +o This provides clear visibility into which document was added, by whom, and at +what level. +``` + +``` +o Documents can be previewed or downloaded directly from the tracker for audit +and compliance review. +o Any re-upload or replacement creates a versioned entry , preserving historical +visibility. +``` +``` +6.20.3.3 Evaluator Tracking & Status Indicators: +``` +``` +o Evaluator details (e.g., DD-ZM, RBM, ZBH, DD-Head, NBH) are shown for each +interview and approval stage. +o Completed steps are marked in green with timestamps; pending stages appear +grey. +o In-progress stages update dynamically as actions are performed. +``` +**6.20.4 Personas-Wise Accessibility & Visibility** + +``` +Persona Accessibility Visibility Scope +DD-ZM / RBM / DD- +Lead / ZBH / DD-Head / +NBH +``` +``` +Can view all stages, download documents, +and verify evaluator remarks for assigned +applications. +``` +``` +Role-based visibility +within region or +zone. +DD-Admin Full access to track overall progress, verify +documents, and audit stage-wise actions. +``` +``` +Complete visibility. +``` +``` +FDD (External Agency) Can upload and review documents related to +financial due diligence but cannot access +internal stages. +``` +``` +Restricted to FDD +workflow stage. +``` +``` +Finance / Legal Can review and upload documents within +assigned compliance or verification stages. +``` +``` +Tag-based visibility. +``` +``` +System (Automation +Layer) +``` +``` +Updates stage completion, uploads +metadata, and logs all actions in the Audit +Trail. +``` +``` +Background +operation. +``` +``` +Applicant + DD ASM +Access +``` +``` +The applicant shall have limited, stage- +specific access to the portal. +The DD ASM is granted access to designated +stages for document upload and +coordination activities. +``` + +### 6.21 Central Document Repository + +**6.21.1 Functionality Scope** + +The **Central Document Repository** serves as a unified digital storage system that consolidates all +applicant documents, artefacts, and submissions across the dealer onboarding and offboarding +lifecycle. +It provides authorized users with **quick, structured, and traceable access** to every document +uploaded during the application process โ€” ensuring consistency, transparency, and audit +readiness across departments. + +**6.21.2 Width** + +- Accessible from the **Documents tab** within each applicationโ€™s detail view and from + the **Central Repository dashboard** for cross-application reference. +- Displays a **tabular view** with the following columns: + o **File Name** (hyperlinked to the document) + o **Type** (e.g., PDF, JPG, XLSX, DOCX) + o **Upload Date** + o **Uploader** (user name and designation) + o **Actions** (download or view document) +- Includes an **Upload Document** button for authorized users to add new or supplementary + files. +- Supports **document preview and download** with automatic logging into the Audit Trail. + + +**6.21.3 Depth** + +- **Document Aggregation:** + o All documents uploaded by the applicant or internal users โ€” across stages such + as Application Submission, FDD, LOI, Statutory, and EOR โ€” are automatically + consolidated here. + o The system captures metadata for each file: + โ–ช File name and format + โ–ช Uploaded by (user and role) + โ–ช Timestamp of upload + โ–ช Stage and status of workflow at upload time + o Ensures centralized visibility and prevents duplication of files across stages. +- **Access & Visibility:** + o Each user role (Admin, Finance, Architecture, Legal, etc.) can view only those files + relevant to their assigned application stage. + o Files uploaded by applicants are visible to authorized RE personnel +- **Data Integrity & Auditability:** + o Every upload, download, or replacement is automatically logged in the **Audit** + **Trail** with timestamp and user identity. + o The repository supports REโ€™s internal compliance requirement for **document** + **retention and traceability** , ensuring complete transparency in every workflow. + +**6.21.4 Personas-Wise Accessibility & Visibility** + +``` +Persona Accessibility Visibility Scope +DD-Admin Full access to upload, view, and download all +documents across applications. +``` +``` +Complete +visibility and +control. +Finance / Legal / +Architecture / Brand +Experience Teams +``` +``` +View and verify documents relevant to their +assigned stages (FDD, LOI, Statutory, EOR). +``` +``` +Role-based +restricted +visibility. +DD-Head / NBH Read-only access to review applicant artefacts +and approvals for final decision-making. +``` +``` +Read-only +visibility. +System (Automation +Layer) +``` +``` +Aggregates documents from all workflow +stages, maintains version control, and syncs +logs with Audit Trail. +``` +``` +Background +operation. +``` +``` +Applicant + DD ASM +Access +``` +``` +The applicant shall have limited, stage- +specific access to the portal. +The DD ASM is granted access to designated +stages for document upload and +coordination activities. +``` + +### 6.22 Audit Trail & Activity Log.......................................................................................... + +**6.22.1 1. Functionality Scope** + +The **Audit Trail & Activity Log** module maintains a complete chronological record of all system +activities, transactions, and user actions performed throughout the dealer onboarding and +offboarding lifecycle. It provides an **immutable, timestamped, and role-based** view of every +workflow step, ensuring **traceability, accountability, and compliance** across all process stages. + +Resignation Sent Back by ZBH โ€“ Remarks posted via Work Notes. +Resignation Sent Back by DD-Lead โ€“ Remarks posted via Work Notes. +Resignation Revoked by NBH โ€“ Action logged with justification. + +**6.22.2 2. Width** + +- Accessible under the **Audit Trail tab** in the **Application Detail View** for every applicant. +- Displays a structured log of all activities related to that specific application. +- Each log entry includes: + o **Action Type** (e.g., Application Submitted, Questionnaire Completed, Interview + Scheduled, LOI Issued). + o **Performed By** (user name and designation, or โ€œSystemโ€ for automated actions). + o **Description / Remarks** (detailing what was done). + o **Timestamp** (exact date and time of the event). +- Entries are displayed in descending chronological order to ensure the latest actions are + always visible. + + +**6.22.3 3. Depth** + +``` +6.22.3.1 Activity Logging +``` +- Every workflow event โ€” from form submission to final approval โ€” is automatically logged + by the system. +- Typical recorded events include: + o Application creation, submission, and status transitions. + o Interview scheduling and completion. + o Document uploads, downloads, and version updates. + o FDD submissions and Finance validations. + o LOI, LOA, and EOR stage completions. + o Inauguration logging and closure actions. + o WhatsApp Notification Triggered โ€“ Questionnaire Reminder Sent to Applicant. +- System-triggered events (e.g., _Questionnaire Link Sent_ , _Automated Email Triggered_ ) are + explicitly marked as performed โ€œby System.โ€ +- User-initiated actions record both name and role for audit clarity. +- Dealer Resignation Initiated by Dealer โ€“ Request submitted via dealer portal. + +``` +6.22.3.2 Description & Detailing +``` +- Each entry provides a concise description of the event and its context. + o Example: + โ–ช _Application Submitted by Amit Sharma โ€” Initial application form_ + _submitted._ + โ–ช _Questionnaire Completed by Applicant โ€” Scored 85/100._ + โ–ช _LOI Approved by DD-Head โ€” Document ready for issue._ +- Any event with system interaction or document movement captures associated metadata + such as document name, file type, and upload path. + +``` +6.22.3.3 Search, Filter & Export Capabilities +``` +- Users can **search** or **filter** logs by: + o Date Range + o Action Type + o User or Role +- The entire log can be **exported as a PDF** for audit or compliance reporting. + +``` +6.22.3.4 Integration with Other Modules +``` +- The Audit Trail is integrated with all system modules, including: + o **Documents Repository:** Logs every upload, download, or version replacement. + + +``` +o Work Notes: Captures internal discussions and query responses. +o Notifications: Records every alert or email triggered by the system. +o Interview Feedback: Stores evaluator submissions and decision timestamps. +o LOI / LOA / EOR Stages: Marks approvals, uploads, and status changes. +``` +- This unified tracking ensures there are **no unrecorded actions** , regardless of user role or + stage. + +``` +6.22.3.5 Security & Data Integrity +``` +- Audit logs are **read-only and non-editable** to preserve authenticity. + +**6.22.4 Personas-Wise Accessibility & Visibility** + +``` +Persona Accessibility Visibility Scope +RE User except +FDD +``` +``` +Full access to view all logs and export reports. Complete visibility and +export control. +System +(Automation +Layer) +``` +``` +Automatically records all workflow events and +triggers background logging for system actions. +``` +``` +Background operation. +``` +## 7 Dealer Resignation + +The **Dealer Resignation** process enables an existing Royal Enfield dealer to formally + +initiate their intent to discontinue the dealership through a structured and transparent +workflow. This process captures the dealerโ€™s resignation details, reasons for exit, and +proposed timeline, ensuring all associated departments โ€” including **DD-Admin, DD-** + +**Head, Finance, Legal, and Regional Teams** โ€” are informed and involved in the validation +and clearance stages. Each resignation request undergoes systematic review, covering + +asset recovery, financial reconciliation, documentation verification, and contractual +obligations before final approval and closure. + + +### 7.1 Dealer Resignation Request (Initiation) + +**7.1.1 Functionality Scope** + +The **Dealer Resignation Request** process begins when a dealer formally communicates their +intent to resign via an **official email** to ASM. Once received, the **DD-ASM** initiates the resignation +process in the system by creating a digital record using the _Create Resignation Request_ form. The +form captures critical dealership, operational, and contextual information โ€” such as business +constitution, sales data, and closure type โ€” ensuring that the request is documented in a +structured, traceable, and standardized manner. This process establishes a single source of truth +for all resignation-related data, facilitating transparent coordination among **DD-Head, Finance, +Legal, and Regional Teams** for subsequent review and action. Dealer can login exclusively and +can only initiate the Resignation request. + +The **Dealer Resignation Request is initiated by the dealer through the portal** , providing a +structured mechanism to formally submit the intent to discontinue the dealership. The dealer +captures resignation details, reason for exit, and the proposed effective date. Upon submission, +the request is routed to the internal stakeholders for review, validation, and subsequent +clearance processes. The **dealer logs into the portal and initiates the resignation request** by +submitting the required details and supporting information. + + +**7.1.2 Width** + +- Accessible exclusively to **DD-ASM** through the **โ€œCreate Resignation Requestโ€** interface. +- Includes the following mandatory and optional input fields: + o **Dealer Code** (it will be fed to SAP API to pull details.) + o **Inauguration** , **LOA** , and **LOI Dates** (Will be fetched from system DB, if available) + o **Last 6 Months Sales** + o **Number of Dealerships / Studios** + o **Constitution** (Proprietorship, Partnership, LLP, Pvt. Ltd., etc.) + o **Dealership Type** (Main, Satellite, Studio, etc.) + o **Type of Closure** (Voluntary, Business Transfer, Termination, etc.) + o **Format Category** (Urban, Rural, etc.) + o **Dealer Scorecard Band** + o **Resignation Reason** (brief summary) + o **Dealer Voice** (detailed justification or remarks from dealerโ€™s email) + o **Upload Document** (resignation email copy or supporting documents) +- **Buttons:** + o **Submit Request:** validates data and triggers routing to the next stage of review. + o **Cancel:** exits without saving. + +**7.1.3 Depth** + +- Upon submission by **DD-Admin** , the system performs the following + o Validates the **Dealer Code** against the dealership master from SAP API to be + provided by RE + o Generates a unique **Resignation Request ID** and logs submission details + (timestamp, user, and role). + o Stores the uploaded resignation email or document in the **Central Document** + **Repository** for reference. + o Automatically notifies the **DD-Head** and relevant stakeholders that a new + resignation has been logged. + o Marks the case status as **โ€œResignation Initiatedโ€** in the workflow tracker. + o He will also upload the resignation PPT which is build off the system. + +**7.1.4 Personas-Wise Accessibility & Visibility** + +``` +Persona Accessibility Visibility Scope +Dealer / +Applicant +``` +``` +Sends official resignation email to Royal Enfield. +The dealer is provided portal access to upload +resignation-related documents and +responses during the applicable workflow +stages. +``` +``` +Email communication +only (no direct system +access). +``` + +``` +For termination cases, dealer upload access is +restricted as per defined governance rules. +DD-Admin/DD- +ASM +``` +``` +Creates resignation request in system, uploads +dealerโ€™s email, validates data, and submits for +approval. +``` +``` +Full access for initiation. +``` +``` +DD-Head / ZBH +/ NBH +``` +``` +Receives system notification upon submission; +can view request details and attached +resignation communication. +``` +``` +Read-only visibility at +initiation stage. +``` +``` +System +(Automation) +``` +``` +Validates dealer code, generates request ID, logs +submission details, and triggers workflow +routing. +``` +``` +Background operation. +``` +### 7.2 Resignation Management Dashboard + +**7.2.1 Functionality Scope** + +The **Resignation Management Dashboard** serves as the central workspace for monitoring and +managing all dealer resignation requests initiated within the system. It provides a consolidated +view of active, pending, and completed cases, enabling stakeholders such as **DD-Admin, ASM, +DD-Lead, ZBH, NBH, and Legal Teams** to review progress, take required actions, and ensure +compliance with the defined offboarding workflow. + +The **ZBH can review resignation requests and perform Send Back or Revoke actions** prior to final +approval. Each action requires **mandatory remarks** and is recorded against the resignation case. + + +RBM, **ZBH, DD-Lead, DD-Head, and NBH** can review resignation requests and are authorized +to **Send Back or Revoke** requests at their respective stages. All such actions require **mandatory +remarks** and are logged for audit purposes. + +**7.2.2 Width** + +- Displays a **summary header** with following key counters: + o **All Requests:** Total number of resignation requests recorded. + o **Open:** Requests currently under review or action. + o **Completed:** Finalized resignations where closure is approved. + o **Requires Your Action:** Highlights cases awaiting action from the logged-in user. +- Shows a **list view** of all resignation requests with the following details: + o **Request ID (e.g., RES-001)** + o **Dealer Name, Dealer Code, and Location** + o **Format Category** (A+, A, B, etc.) + o **Dealership Type** (Main, Studio, etc.) + o **Reason for Resignation** + o **Current Stage** (e.g., ASM Review, DD-Lead Review, NBH Approved, Legal) + o **Submitted On** (auto-captured timestamp) +- Action options: + o **View Details:** Opens complete resignation record and attached documents. + o **Create Resignation Request:** Accessible only to **DD-Admin** for entering new + requests (from dealer emails). +- Filter tabs: + o **All Requests** , **Open** , **Completed** + +**7.2.3 Depth** + +- **Workflow Synchronization:** Each resignation request dynamically updates its stage label + (e.g., _ASM Review_ , _DD-Lead Review_ , _NBH Approved_ ) based on workflow transitions. +- **Notification Logic:** + o The assigned reviewer (ASM, DD-Lead, or NBH) receives automated alerts for + action items. + o Status changes trigger notifications to the next role in sequence. + +**7.2.4 Personas-Wise Accessibility & Visibility** + +``` +Persona Accessibility Visibility Scope +DD-Admin/DD-ASM Can create new resignation requests, view all +regional cases, and track progress. +``` +``` +Full access +within Area. +``` + +``` +ASM / DD-Lead Can review, comment, and forward resignation +requests to next approver. +``` +``` +Assigned +requests only. +ZBH / NBH / Legal / +Finance +``` +``` +Can view status, add remarks, and take action at +their respective workflow stage. +``` +``` +Role-based +access by stage. +System +(Automation) +``` +``` +Updates request stages, triggers notifications, and +logs all workflow events. +``` +``` +Background +process. +Regional Business +Manager (RBM) +``` +``` +The RBM reviews and validates dealer lifecycle +requests , and has review, send back, and +escalation authority as per workflow stage. +``` +### 7.3 Resignation Details & Review + +**7.3.1 Functionality Scope** + +The **Resignation Details & Review** module provides a comprehensive view of all dealer +resignation information captured during initiation. It enables authorized reviewers to validate +dealer data, evaluate the reason and context for resignation, and take appropriate workflow + + +actions such as **Approval, Withdrawal, Send Back, or Push to Full & Final (F&F)**. The screen +consolidates dealer master data, operational metrics, and resignation specifics, ensuring +reviewers have complete visibility before making decisions. + +**7.3.2 Width** + +- **Header Actions:** + o **Approve:** Marks resignation as validated and forwards it to the next workflow + stage (DD-Head / NBH). + o **Withdrawal:** Used if the dealer retracts the resignation request or if withdrawal + is approved internally. + o **Send Back:** Returns the request to DD-Admin for correction or additional details. + o **Push to F&F:** Moves the case to the **Full & Final Settlement** process after all + approvals are secured. + o **Assign User:** Allows reallocation of review responsibility to another internal user. + o **View Work Notes:** Opens the shared comment thread for internal collaboration + and tagging. +- **Tabs:** + o **Details** โ€“ Displays complete resignation information and dealer data. + o **Progress** โ€“ Shows stage-wise workflow journey and current reviewer. + o **Documents** โ€“ Lists uploaded resignation documents and correspondence. + o **Audit Trail** โ€“ Records every action, decision, and timestamp for traceability. + +**7.3.3 3. Depth** + +- **Information Segments:** + o **Request Information:** Pull dealer master details such as Dealer Code, GST, + Address, Domain & Service Codes, City Category, and Dealership Name. + o **Operational Details:** Displays dealership metrics including inauguration and LOA + dates, number of outlets, last six-month sales, business constitution, format + category, and dealer scorecard band. + o **Resignation Details:** Summarizes the **Resignation Reason** and **Dealer Voice** + **(Customer Description)** derived from the dealerโ€™s email submission. + +**7.3.4 4. Personas-Wise Accessibility & Visibility** + +``` +Persona Accessibility Visibility Scope +DD-Admin Read-only at this stage; may receive โ€œSend +Backโ€ tasks for correction. +``` +``` +Region-specific +requests. +``` + +``` +ASM / DD-Lead / DD- +Head / ZBH / NBH +``` +``` +Can review, approve, withdraw, or forward +resignations to next stage; can add remarks +and push to F&F. +``` +``` +Requests assigned to +their hierarchy. +``` +``` +System (Automation) Logs workflow actions, timestamps, and user +activity; triggers stage transitions and +notifications. +``` +``` +Background +operation. +``` +### 7.4 Resignation Request Review & Action Management + +**7.4.1 Functionality Scope** + +The **Resignation Progress Timeline** provides a transparent, stepwise view of the dealer +resignation workflow โ€” from initial submission to the issuance of the final **Acceptance Letter**. +Since the **Dealer does not have portal access** for resignation, the process starts through an **email +submission to the Area Sales Manager (ASM)** , followed by progressive reviews and comments +at multiple organizational levels. Each approver in the chain can perform one of three key actions +โ€” **Approve** , **Send Back for Clarification** , or **Withdraw** โ€” with remarks captured in **Work +Notes** for audit and traceability. Once approved by the **National Business Head (NBH)** , the +request automatically routes to the **Legal Team** for the issuance of the acceptance letter, visible +to both the DD Admin and DD-ASM. + +The **dealer is provided portal access** to **upload resignation-related documents and +responses** during the applicable workflow stages. For termination cases, **dealer upload access is +restricted** as per defined governance rules. + +**7.4.2 Width** + +``` +7.4.2.1 Stage-wise Flow +Stage Responsible +Role +``` +``` +System / Process Description +``` + +1. Dealer +Resignation +Submission + +``` +Dealer โ†’ via +Email to ASM +``` +- Dealer submits resignation via official email and +signed letterhead. +- No direct portal access. +- ASM receives and verifies authenticity. +2. ASM Review DD-ASM โ€ข Uploads resignation email and presentation +(e.g., _Sample resignation.pptx_ ) to portal. +- Adds remarks summarizing dealerโ€™s reason and +operational background. +- Forwards case to **RBM + DD-ZM** for evaluation. +3. RBM + DD-ZM +Review + +``` +RBM & DD-ZM โ€ข Conduct joint discussion with dealer to understand +cause and alternatives. +``` +- Uploads discussion notes and remarks in **Work Notes**. +- The final output will be submitted as Approve, +Withdrawal or send back. +- Has three action options: +- **Approve:** Forwards case to ZBH for further review. +- **Send Back:** Requests ASM to provide additional +details or clarifications (remark mandatory). +- **Withdraw:** Stops process if dealer withdraws or +case found invalid (remark mandatory). +4. ZBH Review Zonal Business +Head +- Reviews RBM + DD-ZM inputs and validates zonal +implications. +- Adds comments in **Work Notes** and forwards to **DD +Lead**. +- Can perform **Approve** , **Send Back** , +or **Withdraw** actions. +5. DD Lead +Review + +``` +DD Lead โ€ข Prepares a formal Resignation Presentation +PPT summarizing business rationale, sales history, +dealer feedback, and proposed recommendation. +``` +- Uploads the presentation and comments to the +portal. +- Approves and shares with **NBH** for final decision. +6. NBH Approval National +Business Head +- Reviews all inputs and puts **final decision remarks** in +Work Notes. +- On approval, system triggers notification to **DD Lead, +ZBH, Zonal Team, Business Zonal Manager, and F&F**. +- Automatically routes the case to **Legal Team** for +Acceptance Letter issuance. +7. Legal Review & +Acceptance Letter + +``` +Legal Team โ€ข Prepares and uploads Resignation Acceptance +Letter on portal. +``` +- Can raise queries in Work Notes if required. +- Uploaded document is visible to **DD-Admin** and **DD-** + + +#### ASM. + +- Legal completion closes workflow for the request. +8. DD Admin & +ASM Notification + +``` +DD Admin + +DD-ASM +``` +- DD Admin reviews the uploaded acceptance letter. +- Shares with respective **ASM (Field Team)** to +communicate official closure to the dealer. + +**7.4.3 3. Depth** + +- **Action Modes Across Stages:** + o **Approve:** Advances the resignation request to the next level of the workflow. + _Example:_ โ€œReviewed with dealer and validated. Forwarding to ZBH for next stage.โ€ + o **Send Back:** Returns to the previous user or ASM for clarifications. + _Example:_ โ€œIncomplete documentation. Dealer statement on financials missing.โ€ + o **Withdraw:** Ends the process if dealer withdraws voluntarily or management + disapproves continuation. + _Example:_ โ€œDealer requested withdrawal of resignation via email dated 15-Oct.โ€ +- **Audit and Transparency:** + o All actions (including remarks, uploads, and timestamps) are auto-captured + in **Work Notes** and the **Audit Trail**. + o Every document and PPT uploaded (e.g., _Sample resignation.pptx_ ) is linked to its + stage for version tracking. +- **System Automation:** + o NBH approval automatically triggers Legal assignment. + o SLA tracking continues at each step; escalation is logged in Work Notes if delayed. + o Notifications are sent to all relevant stakeholders upon approval, send-back, or + withdrawal. + +**7.4.4 Worknotes** + +The **Work Notes** feature acts as the central communication and collaboration thread +within the resignation workflow. It captures all user interactions, remarks, and system- + +triggered updates in a structured, time-stamped format. Each stakeholder โ€” from +ASM to NBH and Legal โ€” uses Work Notes to record discussions, queries, + +clarifications, and final decisions related to the resignation case will be submitted from +Approval, Withdrawal or send back action. + +**7.4.5 Personas-wise Accessibility & Visibility** + +``` +Role / Persona Responsibilities System Access & Actions +``` + +``` +Dealer (External) Submits resignation via email on company +letterhead. +``` +``` +No portal access; +communicates via email +only. +DD-ASM (Area +Sales Manager) +``` +``` +Initiates workflow by uploading resignation +documents, adding dealer background, and +forwarding case. +``` +``` +Create, view, and +comment rights. +``` +``` +RBM + DD-ZM Conduct discussion with dealer, upload +remarks, and validate reasons. +``` +``` +Approve, Send Back, +Withdraw, upload +documents. +ZBH (Zonal +Business Head) +``` +``` +Validate business impact; forward decision +to DD Lead. +``` +``` +Approve, Send Back, +Withdraw rights. +DD Lead Consolidates inputs; prepares final +presentation with recommendations for +NBH. +``` +``` +Approve, Send Back, +Withdraw, upload +presentation. +NBH (National +Business Head) +``` +``` +Takes final decision; puts remarks in system; +triggers Legal action. +``` +``` +Final Approval rights. +``` +``` +Legal Uploads Resignation Acceptance Letter ; +communicates queries in Work Notes. +``` +``` +Upload and comment +rights; visible to DD Admin +& ASM. +DD Admin Reviews uploaded acceptance letter; shares +with ASM for final dealer communication. +``` +``` +Read & Download access. +``` +``` +System +(Automation) +``` +``` +Triggers notifications, maintains SLA +counters, logs Work Notes & Audit history. +``` +``` +Automated access only. +``` +### 7.5 Resignation Progress Tracker + + +**7.5.1 Functionality Scope** + +The **Progress** section provides a stage-wise, visual representation of the entire dealer resignation +workflow. It enables authorized users to track each approval checkpoint โ€” from **request +submission** through **multi-level review** to **final legal acceptance**. Every stage dynamically +updates based on workflow actions such as _Approve_ , _Send Back_ , or _Withdraw_ , with complete +traceability of remarks, uploaded documents, and timestamps. This ensures full transparency, +accountability, and operational consistency across all hierarchical levels. + +**7.5.2 Width** + +- Presents a **chronological timeline** of the resignation process, beginning with _Request +Submitted_ and concluding with _Legal โ€“ Resignation Letter_. +- Each stage displays **status indicators** (Pending, In Progress, Approved, or Withdrawn) along +with the **responsible reviewer role**. +- Shows the **number of documents uploaded** at each stage, with direct view/download options. +- Allows reviewers to perform three key actions โ€” _Approve_ , _Send Back_ , and _Withdraw_ โ€” with +remarks made mandatory. +- If a request is **Sent Back** , it automatically reverts to the previous stage, recording remarks +in **Work Notes** and notifying the concerned user. +- On **Withdrawal** , the timeline is locked and marked _Closed โ€“ Withdrawn_ for historical reference. +- Once **NBH** provides final approval, the request is automatically assigned to **Legal** for +acceptance letter issuance. +- The **Legal stage** finalizes the process upon letter upload, marking the case _Completed_ and +notifying DD-Admin and field hierarchy. + +**7.5.3 Depth** + +- Each stage retains all **remarks, approvals, timestamps, and supporting documents** for +complete traceability. +- Integrates seamlessly with **Work Notes** and **Audit Trail** , ensuring real-time visibility of all +communications and escalations. +- Supports SLA-driven reminders and escalations that reflect directly in the timeline view. +- All uploaded documents (emails, resignation PPT, acceptance letter) remain permanently +mapped to their respective stages. +- Once the resignation is finalized, historical data stays accessible for compliance and audit +review. + + +**7.5.4 Personas-wise Accessibility & Visibility** + +``` +Persona / +Role +``` +``` +Access Level Permissions / Actions Visibility +``` +``` +DD-ASM / +AM +``` +``` +Area Level Uploads the dealerโ€™s resignation +email and supporting PPT; +initiates forwarding for ASM +review. +``` +``` +Can view current stage and +all preceding +remarks/documents. +``` +``` +RBM + DD- +ZM +``` +``` +Regional / Zonal +Level +``` +``` +Reviews and discusses +resignation with the dealer; +provides comments and +forwards to ZBH; can send back +or withdraw. +``` +``` +Can view all area-level +details, remarks, and +uploaded documents. +``` +``` +ZBH Zonal Business +Head +``` +``` +Reviews RBM and DD-ZM +inputs; adds zonal remarks and +forwards to DD-Lead for review. +``` +``` +Can view complete case +details up to current stage. +``` +``` +DD-Lead National +Coordination +Level +``` +``` +Consolidates information; +prepares resignation +presentation with +recommendations; forwards to +NBH. +``` +``` +Can view entire history, +remarks, and document +trail. +``` +``` +NBH National +Business Head +``` +``` +Reviews final presentation; adds +decision remarks; approves +resignation for legal processing. +``` +``` +Can view and comment on +all prior approvals and +documents. +Legal Post-Approval +Level +``` +``` +Uploads the Resignation +Acceptance Letter and closes +the case; can add queries via +Work Notes. +``` +``` +Gains access only after NBH +approval; visible to DD- +Admin upon upload. +``` +``` +DD-Admin Administrative +Level +``` +``` +Reviews and distributes +acceptance letter internally; +ensures completion record +update. +``` +``` +Can view full progress +history and all final +documentation. +``` +``` +All Higher +Roles +(Read- +only) +``` +``` +Oversight Access for viewing status, +remarks, and uploaded files for +compliance or reporting. +``` +``` +View-only access for all +resignation-related data. +``` + +### 7.6 Documents & Audit Trail + +**7.6.1 Functionality Scope** + +The **Documents** and **Audit Trail** sections collectively ensure complete transparency and +traceability across the resignation workflow. The **Documents** tab serves as a centralized +repository of all artefacts submitted or generated during the process โ€” including resignation +letters, presentations, communications, and acceptance letters. The **Audit Trail** automatically +captures every workflow action, recording who performed it, what was changed, and when, +ensuring full accountability and data integrity. + +**7.6.2 Width** + +- Allows upload and viewing of all resignation-related documents with type, uploader, and +upload date clearly listed. +- Supports restricted document viewing to authorized personas with download control. +- Provides versioned tracking of uploaded artefacts for compliance. +- The **Audit Trail** logs every stage transition, approval, comment, or document addition with +precise timestamps. +- Automatically records system-triggered events such as SLA reminders or email notifications. + + +**7.6.3 Depth** + +- Each document remains linked to its respective workflow stage and accessible through +the **Progress Timeline**. +- All actions โ€” _Approve_ , _Send Back_ , _Withdraw_ , _Upload_ , and _Assign_ โ€” are recorded for +traceability. +- The system maintains an immutable historical log for governance and audit purposes. +- Entries in the Audit Trail display both user-driven and automated actions to ensure +comprehensive visibility. + +**7.6.4 Personas-wise Accessibility & Visibility** + +``` +Persona / +Role +``` +``` +Access Level Visibility & Permissions +``` +``` +DD-ASM / +AM +``` +``` +Area Level Can upload resignation email and initial supporting +documents which is the resignation PPT +RBM + DD- +ZM +``` +``` +Regional / Zonal +Level +``` +``` +Can view all uploaded artefacts and upload discussion or +dealer communication documents. +ZBH Zonal Head Can review attached documents and see all prior uploads +with remarks. +DD-Lead National +Coordination +``` +``` +Can upload resignation presentation and verify uploaded +PPT files for completeness. +NBH National Business +Head +``` +``` +Can view all documents and approval history before +finalizing decision. +Legal Post-Approval Level Uploads final Acceptance Letter , visible to DD-Admin and +field teams. +DD-Admin Administrative +Oversight +``` +``` +Has full view and download access to all documents and +audit logs for closure verification. +``` +## 8 Termination + +A **Dealer Termination** process is initiated when a dealershipโ€™s continuation is deemed +non-viable due to business, financial, or ethical reasons. The termination may arise + +from three primary causes โ€” **working capital inadequacy** , **continued underperformance** , +or **unethical practices**. Cases involving working capital or performance issues follow a + +structured review and approval process, allowing the concerned dealer to provide +clarification and supporting data before final decision. However, any instance + +of **unethical practice** โ€” including fraud, policy breach, or reputational risk to the brand +โ€” results in **immediate termination**. All termination cases are documented within the + + +system, with remarks, evidence, and approval trails maintained for audit and +compliance verification. + +### 8.1 Create Termination Request + +**8.1.1 Functionality Scope** + +The **Create Termination Request** form enables authorized users such as **DD-Lead** , **DD-Admin** , +or **ASM** to initiate a termination case within the system. The form captures comprehensive +dealership details including operational timelines, format type, constitution, performance data, +and financial indicators. It also specifies the **Termination Category** (e.g., Working Capital, +Performance Issue, or Unethical Practice), supported by descriptive justification and relevant +documentation. The request forms the starting point of the digital termination workflow and +ensures that all necessary contextual data and artefacts are available for subsequent reviews and +escalations. + +**8.1.2 Width** + +- Allows creation of new termination requests by entering **Dealer Code** , operational details, and +financial data. +- Captures **Termination Category** and **Description** for clarity on grounds of termination. + + +- Supports upload of supporting artefacts such as MOMs, dealer commitments, or financial +statements. +- Automatically records creator and timestamp for traceability. + +**8.1.3 Depth** + +- Integrates directly with the **Progress Timeline** , displaying real-time status updates across levels. +- Each submission auto-generates an internal case ID linked to the dealer code for tracking. +- Supports structured escalation logic based on the **Termination Category** โ€” standard route for +working capital/performance cases, immediate escalation for unethical practices. +- Maintains versioned records for every document uploaded at creation stage. + +**8.1.4 Personas-wise Accessibility & Visibility** + +``` +Persona / Role Access Level Visibility & Permissions +ASM / DD-AM Area Level Can initiate termination requests, upload MOMs and +dealer commitments. +RBM + DD-ZM Regional / Zonal +Level +``` +``` +Can view request details and validate information before +escalation. +ZBH Zonal Head Reviews initial request data, comments on justification, +and forwards to DD-Lead. +DD-Lead / DD- +Admin +``` +``` +National +Coordination +``` +``` +Can initiate, review, and forward requests; validates +completeness and assigns to Legal if required. +Legal Review Level Can view dealer details and supporting documents for +legal evaluation. +NBH National Business +Head +``` +``` +Can view the entire request summary before decision +and closure approval. +``` + +### 8.2 Termination Ticket overview + +**8.2.1 Functionality Scope** + +The **Details View** provides a consolidated summary of all key information related to the dealer +under review. It includes dealership codes, operational history, financial performance, and +termination-specific parameters. This enables reviewers at every levelโ€”whether ASM, ZBH, or +Legalโ€”to quickly assess background context and validate evidence before taking action. The +interface also displays the current workflow stage and offers in-screen options +to **Approve** , **Withdraw** , or **Send Back** the request with remarks, ensuring traceable and reason- +based decisions. + +**8.2.2 Width** + +- Displays complete dealer profile: code, name, location, and GST details. +- Shows operational data: inauguration date, LOA, LOI, format, constitution, and last six-month +sales. +- Captures termination-specific data: **Termination Category** , reason, and case severity (e.g., +โ€œHighโ€). +- Provides workflow action buttonsโ€” **Approve** , **Withdraw** , **Send Back** โ€”with mandatory remarks +input. +- Integration with Work Notes for contextual communication and escalation traceability. + + +**8.2.3 Depth** + +The **Detail Tab** serves as the **central operational dashboard** for viewing all dealer, operational, +and termination-related data within a single, structured interface. It merges static dealer master +information with dynamic workflow inputs and uploaded artefacts, ensuring contextual visibility +for all stakeholders. + +``` +8.2.3.1 Components & Functional Behavior +``` +- **Dealer Information (Owner: DD-Admin / System Integration Layer)** + Displays master data pulled from the Dealer Master table โ€” including **Dealer Code,** + **Name, Address, GST, Domain Name, City Category, Sales Code, Service Code, and GMA** + **Code**. + o Synced automatically from REโ€™s **Dealer Database (Master Registry)**. + o Read-only for all personas except system admin for data correction requests. + o Enables search and cross-referencing across termination, resignation, and + onboarding records. +- **Operational Details (Owner: DD-Lead / Workflow Engine)** + Highlights the dealershipโ€™s business health indicators and structural data, including **LOA,** + **LOI, Inauguration Date, Constitution Type, Dealership Type, Format Category, Dealer** + **Score Card Band, and Last Six-Month Sales**. + o Pulled dynamically from the Sales & Performance Module. + o Reflects the most recent sales cycle, ensuring leadership sees live performance + metrics during termination decision-making. + o Editable only by DD-Lead or authorized DD-Admin prior to case lock. +- **Termination Details (Owner: DD-Lead / DD-ZM / Legal)** + Captures case-specific details such as **Termination Category, Reason Description, and** + **Attachments**. + o Termination Category includes options like _Working Capital Issues, Performance_ + _Shortfall, Breach of Agreement, or Unethical Practices_. + o Documents uploaded here are visible to all reviewers across the approval chain, + maintaining transparency. + o Legal team references this section while framing the **Show Cause Notice (SCN)** or + final termination letter. +- **Workflow Actions (Owner: Workflow Engine / DD-Lead)** + Displays **Approve, Withdraw, and Send Back** controls based on role permissions. + o Triggers automated workflow transitions and real-time updates in **Progress** + **Timeline** and **Audit Trail**. + o Any action logs mandatory remarks under โ€œCommunication & Notesโ€ with + timestamp and user identity. + o Permissions vary per role: + + +``` +โ–ช ASM, RBM: Can only comment or escalate. +โ–ช ZBH, DD-Lead, NBH: Can approve or send back. +โ–ช Legal: Can finalize after NBH approval. +``` +- **Document Management Section (Owner: DD-Admin / Legal)** + Repository displaying all uploaded evidence or reports associated with the termination. + o Documents listed by **name, type, uploader, and date**. + o Supports inline viewing (no download needed) for internal confidentiality. + o File retention policy aligns with REโ€™s compliance standards (minimum 7 years). +- **Audit Trail (Owner: Workflow Engine / System Log)** + Chronologically records every action taken within the termination case โ€” including + user, timestamp, and nature of change. + +**8.2.4 Personas-wise Accessibility & Visibility** + +``` +Persona / Role Access Level Visibility & Permissions +ASM / DD-AM Area Level Can initiate and upload dealer MOMs and commitment +records. +RBM + DD-ZM Regional / Zonal +Level +``` +``` +Review dealer details, validate termination rationale, +and escalate with remarks. +ZBH Zonal Business +Head +``` +``` +Approves or returns the case with comments; can +forward to DD-Lead. +DD-Lead / DD- +Admin +``` +``` +National +Coordination +``` +``` +Validate details, review documents, assign to Legal, or +push for F&F after NBH approval. +Legal Legal Level Review dealer information, validate grounds, and issue +termination letter. +NBH National Head Provides final decision and authorization before case +closure. +``` +### 8.3 Termination Approval & Review Process + +**8.3.1 Functionality Scope** + +The **Termination Approval module** enables Royal Enfieldโ€™s internal stakeholders to manage +dealership termination cases in a structured, transparent, and traceable workflow. It ensures that + + +every dealership performance concern โ€” whether due to **working capital shortfall** , **sustained +underperformance** , or **unethical practices** โ€” is systematically reviewed, documented, and acted +upon through the defined escalation hierarchy. + +This module supports structured documentation of **dealer meetings** , **uploaded +artefacts** , **reviewer remarks** , and **legal correspondence** , ensuring no manual communication +dependency. +All approvals, send-backs, or withdrawals are centrally logged, supported by **Work Notes** , +ensuring collaborative clarity and institutional memory across teams. + +The **CEO is the final approving authority** for dealer termination cases. The **Legal team prepares +and issues the termination letter only after CEO approval** , and **not upon NBH approval**. +**CCO and CEO** are included as approval authorities with **Approve, Hold, and Reject options**. +The **dealer does not have portal access** for termination workflows. + +**8.3.2 Width** + +The process spans across the complete DD and Legal hierarchy, ensuring clear role-based +accountability: + +- **ASM:** Conducts monthly visits, logs Meeting of Minutes (MOM), uploads dealer + commitment letter and personal observations. Logging MOM is not the part of this system + but when he feel to trigger Termination, he will log as description & associate documents + while initiating the flow. +- **RBM + DD-ZM:** Escalate after repeated concerns, conduct joint meetings, and document + dealer responses on portal. +- **ZBH:** Reviews zonal-level non-compliance, escalates unresolved cases to DD-Lead and + NBH. +- **DD-Lead:** Reviews consolidated reports, validates escalation records, prepares case + presentation, and assigns to Legal. +- **Legal:** Reviews chronology, evaluates policy or contractual breaches, issues SCN, and + prepares final Termination Letter. +- **DD-Head:** Reviews with DD-Lead and Legal; presents case to NBH for decision. +- **NBH:** Provides final decision โ€“ approve, query, or hold. +- **DD-Admin:** Uploads dealerโ€™s SCN response and handles F&F coordination post Legal + issuance. + + +**8.3.3 Depth** + +- **Structured Case Creation (Owner: DD-Lead / DD-Admin / ASM)** + A Termination case is initiated through the โ€œCreate Termination Requestโ€ form by DD- + Lead, DD-Admin, or ASM. + o Each request is tagged with a unique **Termination ID** (e.g., TERM-001). + o Dealer and operational data are automatically fetched from the **Dealer** + **Master** and **Sales System** for accuracy. +- **Case Workflow Management (Owner: Workflow Engine)** + Each stage of the termination journey โ€” from ASM initiation to Legal closure โ€” is + mapped to approval levels. + o **ASM โ†’ RBM/DD-ZM โ†’ ZBH โ†’ DD-Lead โ†’ Legal โ†’ DD-Head โ†’ NBH**. + o Actions at every level (Approve, Withdraw, Send Back) are recorded with + mandatory remarks. + o Each remark auto-updates in **Work Notes** and **Progress Timeline** , triggering + instant notifications to the next role. +- **Work Note Integration (Owner: All Reviewers)** + The **Work Note** acts as the **central communication thread** within each termination case. + o Each reviewer (ASM, RBM, ZBH, DD-Lead, Legal, etc.) can post contextual remarks, + share discussions, or tag specific users. + o Tagged users (e.g., @DD-Lead, @Legal) receive instant notifications via **system** + **alerts** and **email**. + o Work Notes serve as a real-time collaboration and escalation record โ€” every + comment, clarification, or update remains **time-stamped and user-tagged**. + o Legal and DD-Head may also use Work Notes to request clarification from lower + hierarchies (ASM, RBM, ZBH). + o Once a note is submitted, it becomes immutable and part of the **permanent** + **record** under **Audit Trail**. +- **Meeting & Artefact Uploads (Owner: ASM, RBM, ZBH)** + Each level of escalation includes upload of MOMs, dealer commitment letters, and + observations while Approving at his level. + o Artefacts are uploaded as PDFs (e.g., _Meeting_MOM_June2025.pdf_ ). + o Dealer commitments are scanned and attached for cross-reference during Legal + and NBH reviews. +- **Approval Actions (Owner: Workflow Engine)** + Reviewers can take the following actions: + o **Approve:** Confirms escalation readiness for next level. + o **Send Back:** Pushes case back for clarification with remarks visible in Work Notes. + o **Withdraw:** Used when the concern is resolved or no termination action is required. + Each action is recorded in both **Audit Trail** and **Work Notes** , ensuring clarity on + decision paths. + + +- **Legal Review and Issuance (Owner: Legal Team)** + Legal reviews the case chronology and uploaded artefacts. + o If clarification is needed, they โ€œSend Backโ€ via Work Notes. + o Once validated, Legal create the **Show Cause Notice (SCN)** to the portal and later + create the **Termination Letter** post NBH approval. + o These Show cause Notice and Termination Letter will be created within the system + o All uploaded legal artefacts remain accessible to DD-Lead, DD-Admin, and NBH. +- **Dealer Interaction & Closure (Owner: DD-Admin / DD-Lead)** + Dealer replies to the SCN via DD-Admin, who uploads the response to the portal. + o DD-Lead reviews dealerโ€™s response with inputs from RBM and ZBH, updates + closure remarks, and forwards to NBH. + o Post-approval, Legal uploads the Termination Letter, visible to DD-Admin and + dealer. + o DD-Admin initiates **F&F** coordination, ensuring all records are finalized within SLA. +- **Immediate Termination (Owner: DD-Lead + Legal)** + Cases categorized under โ€œUnethical Practiceโ€ trigger direct routing to Legal + DD- + Lead, skipping intermediate reviews. + o Immediate Legal action and issuance of termination communication occur within + the system, ensuring swift compliance. +- **Audit Trail (Owner: System Engine)** + Each user action โ€” approval, send back, upload, comment โ€” is timestamped and + permanently logged. + o The trail captures: _User Name, Action Type, Timestamp, Remarks Summary, and_ + _Linked Artefact_. + o Accessible by DD-Lead, Legal, DD-Head, and NBH for compliance review. + +**8.3.4 Personas-wise Accessibility & Visibility** + +``` +Persona Responsibilities & Key Actions Access Rights +ASM Creates termination request, uploads MOM & dealer +commitments, adds initial remarks and observations. +``` +``` +Create, View, +Comment +RBM / DD- +ZM +``` +``` +Reviews ASM input, conducts escalation meetings, +uploads MOM, provides joint recommendations. +``` +``` +View, Approve, +Send Back +ZBH Reviews regional non-compliance, uploads MOM, +forwards unresolved cases to DD-Lead. +``` +``` +Approve, Send +Back +DD-Lead Reviews full chronology, validates artefacts, triggers Legal +for input, issues SCN, consolidates for final closure. +``` +``` +Full Access, +Approve, +Withdraw +Legal Reviews chronology, uploads SCN, issues Termination +Letter, queries if required through Work Notes. +``` +``` +Approve, Send +Back, Upload +DD-Head Reviews consolidated cases, presents them to NBH for +final decision. +``` +``` +Review, Comment +``` + +``` +NBH Approves or holds termination case; final authority on go- +ahead decisions. +``` +``` +Approve / Hold +``` +``` +DD-Admin Uploads dealerโ€™s SCN reply, final Termination Letter, and +initiates F&F. +``` +``` +Upload, Close +``` +``` +Dealer +(Read-only) +``` +``` +Views SCN and final Termination Letter. View Only +``` +### 8.4 Termination Progress Timeline + +**8.4.1 Functionality Scope** + +The **Termination Progress Timeline** provides a stage-wise visualization of the entire termination +journey โ€” from case initiation to final closure. It ensures that every escalation, document, review, +and approval is tracked transparently with timestamped accountability. + +Each level in the workflow โ€” from **ASM initiation** to **CEO authorization** โ€” is dynamically +reflected with role names, document counts, feedback notes, and status indicators. +The module promotes structured collaboration by integrating **Work Notes** and **Audit +Trail** updates at each milestone, enabling leadership to monitor the decision flow in real time. + + +**8.4.2 Width** + +The timeline consolidates inputs from multiple roles, creating an end-to-end view of operational, +business, and legal evaluations: + +- **ASM** initiates the request and uploads meeting artefacts. +- **RBM / DD-ZM** review and escalate based on repeated violations. +- **ZBH** performs zonal validation and comments. +- **DD-Lead** consolidates data, reviews chronology, and assigns to Legal. +- **Legal** verifies contract breaches and provides legal opinion or Show Cause Notice (SCN). +- **NBH** performs business-level evaluation and grants or holds final approval. +- **CEO / CCO** complete the executive authorization. +- **DD-Admin** coordinates issuance of the final Termination Letter and forwards it to F&F. + +Each transition (approve, send-back, withdraw) automatically updates the timeline with the +reviewerโ€™s remarks and uploaded artefacts. + +**8.4.3 Depth** + +The Termination Progress Timeline follows a clearly defined 14-stage lifecycle. Each stage is +associated with specific ownership, document uploads, and Work Note actions. + +``` +8.4.3.1 Stage-wise Breakdown +``` +1. **Request Initiated** โ€“ _ASM / Initiator_ + o Case created with details, termination reason, and dealer code. + o Supporting documents like MOM and commitment letters attached. + o Remarks and feedback logged in Work Notes. +2. **RBM Review** โ€“ _RBM + DD-ZM_ + o Joint meeting notes uploaded; recommendations shared. + o Approve or Send-Back with clarification via Work Note. +3. **ZBH Review** โ€“ _Zonal Business Head_ + o Evaluates pattern of violations, reviews MOM chain, and adds escalation remarks. +4. **DD Lead Review** โ€“ _DD-Lead_ + o Consolidates documentation from ASM, RBM, and ZBH. + o Prepares case synopsis and assigns to Legal for compliance validation. +5. **Legal Verification** โ€“ _Legal Department_ + o Reviews breach type (Working Capital, Performance, Unethical Practice). + o Queries or approves via Work Notes. + o Uploads draft SCN if verified. +6. **NBH Evaluation** โ€“ _National Business Head_ + + +``` +o Reviews termination recommendation; may approve, hold, or query. +``` +7. **Show Cause Notice (SCN)** โ€“ _Legal + DD-Lead_ + o Official SCN issued to dealer. + o Dealer reply awaited; all correspondence uploaded. +8. **DD Lead & Legal Review** โ€“ _Joint Review_ + o Evaluates dealerโ€™s SCN reply. + o Records internal discussion outcome in Work Notes. +9. **DD-Head Review** โ€“ _Dealer Development Head_ + o Prepares presentation and recommendation for NBH. +10. **CCO Approval** โ€“ _Chief Commercial Officer_ + o Reviews and endorses NBHโ€™s decision. +11. **CEO Final Approval** โ€“ _Chief Executive Officer_ + o Authorizes final termination execution. +12. **Legal โ€“ Termination Letter** โ€“ _Legal Team_ + o Uploads signed Termination Letter to portal. + o Triggers auto-notifications to DD-Lead and DD-Admin. +13. **DD-Admin โ€“ Share with Dealer** โ€“ _DD-Admin_ + o Forwards Termination Letter to dealer. + o Initiates F&F process and records completion date. +14. **Dealer Terminated** โ€“ _System Generated_ + o Marks dealership status as โ€œTerminated.โ€ + o Case locked for further edits; all data archived under Audit Trail. + +``` +8.4.3.2 Work Note Integration +``` +- Each stage allows the reviewer to post contextual **Work Notes** for coordination, + clarification, or escalation. +- Notes automatically capture **author, timestamp, and linked stage**. +- Tagged users receive both **email** and **in-app alerts**. +- Work Notes act as the **single source of truth** , capturing every internal discussion and + external clarification. +- Once the case reaches โ€œDealer Terminated,โ€ Work Notes are archived as part of the + official record visible under **Audit Trail**. + +**8.4.4 Personas-wise Accessibility & Visibility** + +``` +Persona Visibility in Timeline Actions Allowed +ASM Initiate request, view complete history, comment +in Work Notes. +``` +``` +Create, Upload Docs, +Comment +RBM / DD-ZM See all lower-level stages, add remarks, approve or +send-back. +``` +``` +Approve, Send-Back, +Comment +ZBH Access RBM & ASM artefacts, escalate to DD-Lead. Approve, Send-Back +``` + +``` +DD-Lead Full timeline visibility, assign to Legal, manage SCN, +approve final closure. +``` +``` +Full Access +``` +``` +Legal Review termination grounds, issue SCN, upload +Termination Letter. +``` +``` +Approve, Send-Back, +Upload Docs +NBH View all previous stages, make go/no-go decision. Approve / Hold +CCO / CEO Executive-level read access, approve final +termination. +``` +``` +Approve Only +``` +``` +DD-Admin View complete timeline, upload dealer response & +Legal letter, initiate F&F. +``` +``` +Upload, Close +``` +``` +Dealer (Read- +only) +``` +``` +View SCN and Termination Letter post-issuance. View Only +``` +## 9 Admin Section + +### 9.1 Master Configuration โ€“ Organization + +**9.1.1 1. Functionality Scope** + +The **Master Configuration** module forms the foundation of the systemโ€™s administrative and +organizational setup. Within this, the **Regional Hierarchy & Zone Management** section enables +the **System Administrator** to define, structure, and maintain Royal Enfieldโ€™s official **Dealer** + + +**Development (DD) hierarchy** , ensuring every dealer, outlet, and user is correctly mapped to their +respective **Zone, Region, and Area**. This configuration drives workflow routing, approval +ownership, SLA tracking, and reporting alignment across all dealer-facing processes such as +onboarding, resignation, and F&F closure. + +**9.1.2 2. Width** + +- **Regional Hierarchy Overview:** + o Displays five zones โ€” **North, South, East, West, and Central** โ€” each summarizing: + โ–ช Total **Regions** under the zone + โ–ช Number of **Zonal Managers (ZMs)** , **Regional Business Managers (RBMs),** + **Area Sale Manager (ASM) & DD-AM (DD-Area Manager)** + โ–ช + o **Zone Management** grid provides: + โ–ช **Zone Code** , **Zone Name** , and **Region** + โ–ช **States and Areas Covered** + โ–ช **DD / ZM / RBM / ASM / DD-AM Counts** + โ–ช Action controls โ€” **Edit** , **Delete** + โ–ช **Add Zone** option for creating new records +- **Add/Edit Zone Form:** + o Input fields available for setup: + โ–ช **Zone Code** โ€“ e.g., _N-Z1_ + โ–ช **Zone Name** โ€“ e.g., _North Zone_ + โ–ช **Region** โ€“ Select from dropdown ( _UP, Punjab & others_ ) + โ–ช **States Covered** โ€“ Multi-select dropdown populated from the Location + Master + โ–ช **Areas Covered** โ€“ Multi-select dropdown (district- or city-level sub- + mapping) + o **Save Zone:** Validates and commits configuration changes. + o **Cancel:** Closes form without saving. + +**9.1.3 3. Depth** + +- **Definition of Hierarchy:** + o The Royal Enfield network is structured as: + **Zone โ†’ Region โ†’ Area โ†’ Dealer / Showroom** + o Each level has clear administrative and operational ownership, ensuring + traceability and accountability across the dealer ecosystem. + o **Example โ€“ North Zone Structure:** + o North Zone ( 900 Dealers) + o โ”œโ”€โ”€ UP Region ( 180 Dealers) + + +``` +o โ”‚ โ”œโ”€โ”€ Lucknow Area ( 8 Dealers) +o โ”‚ โ”‚ โ”œโ”€โ”€ Pushp Auto (Alambagh Showroom) +o โ”‚ โ”‚ โ”œโ”€โ”€ Rishabh Motors (Gomti Nagar) +o โ”‚ โ”‚ โ””โ”€โ”€ 6 More Local Outlets +o โ”‚ โ”œโ”€โ”€ Kanpur Area ( 10 Dealers) +o โ”‚ โ””โ”€โ”€ 13 Other Areas +o โ”œโ”€โ”€ Punjab Region ( 150 Dealers) +o โ””โ”€โ”€ 5 Other Regions +o This hierarchical configuration ensures that every Dealer , Studio , or Outlet is +mapped under a defined Area , which rolls up into a Region , and subsequently into +a Zone. +``` +- **Data Mapping & Validation Logic:** + o Each **Zone** is assigned a unique identifier and linked to its parent **Region** and **Area**. + o **Dealer Development (DD)** resources are mapped to their respective areas for + process routing. + o Stateโ€“Area relationships are validated to prevent overlapping coverage or + duplicate entries. + o Automatic recalculation of counts occurs when dealers or managers are + reassigned. + +**9.1.4 Dealer Development Hierarchy & Responsibility Mapping** + +``` +Hierarch +y Level +``` +``` +Example from North +Zone Structure +``` +``` +Approx. +Dealer +Coverag +e +``` +``` +Responsible +Roles +``` +``` +Scope of +Oversight / +Visibility +``` +``` +Operational +Responsibilitie +s +``` +``` +National +Level +``` +``` +Pan-India โ€“ All Zones +(North, South, etc) +``` +#### ~3,000+ + +``` +Dealers +``` +``` +DD-Lead , DD- +Head , NBH , DD +``` +**- Admin** + +``` +End-to-end +national +governanc +e across all +Zones, +Regions, +and Areas +``` +- Oversee all +onboarding, +resignation, +and F&F +workflows +- Monitor SLA +adherence and +performance +metrics +- Approve +escalated cases +or exceptions +Zone +Level + +``` +North Zone (e.g., +900 Dealers) +``` +#### 700 โ€“ + +#### 1,000 + +``` +Dealers +per Zone +``` +``` +DD-ZM , ZBH Zonal +oversight +covering +multiple +Regions +``` +- Review zonal +performance +- Coordinate +between +Regional and + + +``` +and their +assigned +RBMs +``` +``` +National teams +``` +- Validate +dealer +onboarding, +closure, and +SLA metrics +- Escalate +financial and +compliance +matters to DD- +Lead +Region +Level + +``` +UP Region , Punjab +Region , etc. +``` +#### 100 โ€“ 200 + +``` +Dealers +per +Region +``` +``` +RBM (Regional +Business +Manager) +``` +``` +Regional +oversight +covering +multiple +Areas +under one +Region +``` +- Supervise +Area Managers +- Approve +dealer-level +operational +activities +- Ensure +adherence to +regional sales, +service, and +brand +standards +- Review and +forward +approvals to +DD-ZM or +higher +Area +Level + +``` +Lucknow Area (8 +Dealers), Kanpur +Area (10 Dealers) +``` +#### 5 โ€“ 15 + +``` +Dealers +per Area +``` +``` +DD-AM (Area +Manager) , ASM +(Area Sales +Manager) +``` +``` +Area-level +operations +covering +dealers and +sub- +dealers +``` +- Manage +direct dealer +interactions +and field audits +- Validate +dealer data, +documents, +and site +activities +- Report +progress, +feedback, and +resignation +inputs + + +``` +upstream +``` +- First point of +verification for +dealer +submissions +Dealer / +Outlet +Level + +``` +Pushp Auto +(Alambagh) , Rishab +h Motors (Gomti +Nagar) +``` +``` +1 Dealer +(Main / +Studio / +Service +Outlet) +``` +``` +DD-AM (Area +Manager) , ASM +(Area Sales +Manager) +``` +``` +Dealer +operations +reporting +into Area- +level roles +``` +- Submit +onboarding, +resignation, or +compliance +documentation +- Coordinate +with DD-AM / +ASM for all +operational +requests + +### 9.2 Zone, Region & Area Configuration + + + +**9.2.1 Functionality Scope** + +The **Zone, Region & Area Configuration** module defines the geographical and managerial +hierarchy governing Royal Enfieldโ€™s entire dealer network. It empowers the **Admin** to configure +the structural mapping of **Zones** , **Regions** , and **Areas** , thereby aligning operational +responsibilities and approval flows across Dealer Development (DD), Sales, and Business teams. +Each **Zone** is led by a **Zonal Business Head (ZBH)** and comprises multiple **Regions** managed +by **Regional Business Managers (RBMs)**. Within each region, **Area Sales Managers +(ASMs)** and **Dealer Development Area Managers (DD-AMs)** oversee localized dealer +operations. Above these field roles, the hierarchy extends to **Dealer Development Zonal +Managers (DD-ZM)** , **DD-Lead** , **DD-Head** , and finally the **National Business Head (NBH)** , +ensuring visibility and governance across all levels. +This structure serves as the foundation for all workflows โ€” including **Onboarding, Resignation, +Termination, and F&F Settlements** โ€” ensuring automated routing, escalation, and +performance tracking aligned to the correct operational hierarchy. + +**9.2.2 Width** + +This module spans the full organizational hierarchy and covers all geographic and managerial +relationships that define how workflows are routed and governed: + + +- **Zone Configuration** + o Define zone code, name, and description (e.g., North Zone, South Zone). + o Assign the **Zonal Business Head (ZBH) & DD-ZM** with name, contact, and email. + o Select all **states and union territories** falling under the zoneโ€™s jurisdiction. +- **Regional Configuration** + o Create one or multiple regions (Sates) under each zone. + o Assign a **Regional Business Manager (RBM)** and link them with contact details. + o Map states and districts under the region. + o Specify the total **Regional Officers** and **Area Managers** working under the region. +- **Area Configuration (ASM / DD-AM Assignment)** + o Configure **Area Sales Managers (ASM)** and **Dealer Development Area Managers** + **(DD-AM)** with designated city or district coverage. + o Link each ASM/DD-AM to their corresponding Region and Zone. + o Set contact details, status, and operational scope (Active/Inactive). +- **Hierarchy Linkage Across Levels** + o Each region and area automatically link upward to **DD-ZM** , **ZBH** , and **DD-Lead** , + ensuring system-level routing alignment. + o National roles such as **DD-Head** and **NBH** inherit macro-level visibility across all + configured territories. + +**9.2.3 Depth** + +- **Organizational Mapping:** + Each dealer request or case โ€” whether onboarding, resignation, or termination โ€” + inherits its routing chain from this hierarchy (e.g., ASM โ†’ RBM โ†’ ZBH โ†’ DD-Lead โ†’ + NBH). This ensures consistent reporting and accountability. +- **Role Interlinking:** + o **ASM & DD-AM** : Manage dealer operations, visit tracking, and initial-level data + inputs. + o **RBM & DD-ZM** : Conduct mid-level evaluations and provide regional performance + oversight. + o **ZBH** : Supervises zonal dealer network health and strategic decisions. + o **DD-Lead & DD-Head** : Manage pan-India dealer policies, escalations, and + workflow resolutions. + o **NBH** : Holds final oversight and decision authority for national-level approvals. +- **Geographic Traceability:** + Every configuration entry โ€” from zone to district โ€” enables traceable linkage of dealer + location, responsible officers, and workflow approvals. +- **Dynamic Updates & Scalability:** + Admins can modify or reassign any role or coverage area without disrupting ongoing + workflows. The system auto-updates workflow routing, escalation hierarchy, and + reports in real time. + + +- **There can be multiple users mapped at same role.** For example, there can be 2 ZBH or 3 + DD-ZM at a same Zone. +- **Data Consistency & Integration:** + Each change reflects across dependent modules like **Role & Permissions** , **SLA** + **Management** , and **Email Notifications** , ensuring all updates remain synchronized. + +**9.2.4 Personas-wise Accessibility & Visibility** + +``` +Persona Responsibilities in Module Access Level +Admin Creates, edits, and manages the entire hierarchy โ€” +zones, regions, and ASMs. Assigns officers and +maintains real-time linkage between geographic and +managerial structures. +``` +``` +Full Access +``` +``` +DD-AM (Dealer +Development Area +Manager) +``` +``` +Views assigned area (district/city), local dealers, and +reporting managers. +``` +``` +View Only +``` +``` +ASM (Area Sales +Manager) +``` +``` +View assigned territoryโ€™s dealer operations, monitors +requests, and coordinates with RBM. +``` +``` +View & +Comment +RBM (Regional +Business Manager) +``` +``` +View regional offices, assigns ASMs, and validates +dealer-level data. +``` +``` +Edit within +assigned +region +DD-ZM (Dealer +Development Zonal +Manager) +``` +``` +Reviews dealer development operations within the +zone and collaborates with RBMs. +``` +``` +View & +Comment +``` +``` +ZBH (Zonal Business +Head) +``` +``` +Monitors zone-level performance and ensures +escalation or workflow alignment with DD-Lead. +``` +``` +View & +Comment +DD-Lead Reviews configuration consistency, ensures correct +routing for all workflows, and validates escalation +logic. +``` +``` +View Only +``` +``` +DD-Head Reviews national-level structure, oversees zonal and +regional performance, and approves any +configuration realignment. +``` +``` +View Only +``` +``` +NBH (National +Business Head) +``` +``` +Holds complete top-level visibility for all zones, +oversees configuration for governance and reporting +accuracy. +``` +``` +View Only +``` + +### 9.3 Roles & permissions + + +**9.3.1 Functionality Scope** + +The **Roles & Permissions** module governs how users across Royal Enfieldโ€™s Dealer Development +and allied departments (Finance, Legal, FDD) interact with the system. +It ensures each role has controlled access to relevant workflows, reports, and actions within +the **Dealer Lifecycle** โ€” from opportunity creation and onboarding, through evaluation and FDD, +to resignation and closure. The module supports **multi-level permission granularity** , allowing +both **role-based** and **user-specific** privilege configurations. This provides flexibility to assign +additional or restricted access based on operational necessity while maintaining organizational +compliance and hierarchy alignment. + +**9.3.2 Width** + +- **Role Management Dashboard:** + o Displays every configured role along with its assigned permissions and mapped + users. + o Columns include: + โ–ช **Role Name** + โ–ช **Permissions** (summary + expandable list) + โ–ช **User Count** + โ–ช **Actions** ( _Edit_ , _Delete_ ) +- **Add/Edit Role:** + o **Role Name** โ€“ unique identifier (e.g., _DD-ZM_ , _Finance_ , _Legal_ ). + o **Description** โ€“ outlines the roleโ€™s scope (e.g., _Manages Zonal Operations & Level- 1_ + _Evaluation_ ). + o **Permission Toggles:** + โ–ช View / Review / Approve / Reject Applications + โ–ช Upload Documents + โ–ช Schedule Interviews + โ–ช Manage Users + โ–ช View Reports + โ–ช Configure SLA + โ–ช Manage Templates + โ–ช View / Verify Payments +- **Save Role / Cancel** โ€“ commits or discards changes. + + +**9.3.3 Depth** + +``` +9.3.3.1 Role Responsibilities & Hierarchy Mapping +Level / Function Roles Involved Scope of Responsibility Core Permissions +Area Level +(Field +Operations) +``` +``` +DD-AM (Dealer +Development +Executive / Area +Manager) +``` +``` +Identifies new dealership +opportunities on-ground, +interacts with prospects, +validates field data, and +supports documentation +readiness. +``` +``` +View & upload +applications, update +opportunity details, +add work notes. +``` +``` +Level- 1 +Evaluation +(Zonal / +Regional +Assessment) +``` +``` +DD-ZM + RBM Conducts initial evaluation +using KT Matrix , reviewing +applicant credentials, +financial potential, and local +market understanding. +``` +``` +View, review, and +approve Level- 1 +applications; record KT +scores; schedule +interviews. +Level- 2 +Evaluation +(Strategic +Assessment) +``` +``` +DD-Lead + ZBH Reviews shortlisted +applications for business +alignment, operational +readiness, and strategic fit. +Approves or forwards for +final evaluation. +``` +``` +Approve/Reject +applications, review +interview feedback, +upload evaluation +documents. +``` +``` +Level- 3 +Evaluation +(National +Approval) +``` +``` +NBH + DD-Head Conducts final decision- +making for dealership +onboarding or closure, +ensuring alignment with +brand growth and financial +feasibility. +``` +``` +Full visibility of all +applications, approve +or reject at final stage, +review all attachments +and reports. +``` +``` +Financial Due +Diligence (FDD) +``` +``` +FDD Team Performs external financial +due diligence for assigned +applications. Limited view of +assigned cases only. Can +upload FDD reports and raise +work notes for queries. +``` +``` +Upload FDD report, add +comments in work +notes, mark +completion. +``` +``` +Finance Finance Team Manages payment-related +verifications, security deposit +validations, and refund +approvals for resignations. +``` +``` +View and verify +payments, upload +supporting documents, +confirm receipts. +Legal Legal +Department +``` +``` +Reviews LOI, LOA, dealership +termination letters, and +agreement documents for +compliance. +``` +``` +View legal documents, +upload vetted files, +provide legal remarks, +``` + +``` +approve or return for +correction. +National +Governance +``` +``` +DD-Lead, DD- +Head, NBH, DD- +Admin +``` +``` +Central oversight for all +zones; monitors workflows, +SLA compliance, and +manages role/user +configurations. +``` +``` +Full system visibility, +manage roles, +configure SLA, access +reports and audit logs. +``` +``` +9.3.3.2 Customizable Permission Framework +``` +- **Role-Level Permissions:** + Define the baseline privileges for all users under a given role (e.g., all DD-ZMs can _view_ + _applications_ , _review_ , and _approve_ Level-1). +- **User-Level Overrides:** + Allow case-specific adjustments for individuals. + o Example: Two DD-ZMs under different zones โ€” one may have additional + permission to _view reports_ , while another may be limited to _review and approve_ + _applications_ only. +- This layered model ensures consistency in role design while supporting operational + adaptability. + +``` +9.3.3.3 Audit & Security Controls +``` +- Every permission change (at both role and user levels) is logged under **Audit Trail** with + timestamp, actor ID, and before-after states. +- Ensures traceability of configuration changes for compliance with Royal Enfieldโ€™s data- + governance framework. +- System auto-validates access inheritance to prevent privilege conflicts between + dependent modules. + +**9.3.4 4. Personas-Wise Access Summary** + +``` +Persona / +Role +``` +``` +Level Operational Focus Permission Highlights +``` +``` +DD-AM Area Ground opportunity +identification, field +validation +``` +``` +Add work notes +``` +``` +RBM Regional Regional evaluation & KT +Matrix scoring +``` +``` +Review documents, add remarks, +shortlist +DD-ZM Zonal Zonal evaluation & Level- 1 +approval +``` +``` +Approve Level-1, manage users +``` + +``` +ZBH Zonal Strategic oversight & Level- 2 +evaluation +``` +``` +Approve Level-2, upload summary, +finalize recommendations +DD-Lead National Governance & performance +oversight +``` +``` +Manage users, approve Level- 2 +evaluations +DD-Head National Final authorization Full system access, finalize +decisions +NBH National Strategic business head Joint Level- 3 evaluation, +approve/reject final decision +FDD External Financial due diligence Upload FDD reports, query via work +notes +Finance Cross- +functional +``` +``` +Payment validation & +security deposit checks +``` +``` +View/verify payments, upload +receipts +Legal Cross- +functional +``` +``` +Legal document review Upload & verify legal documents, +add remarks +DD-Admin National Configuration management Manage roles, SLA, locations, +templates, schedule interviews +``` +### 9.4 SLA Configuration & Escalation Management + + +**9.4.1 Functionality Scope** + +The **SLA Configuration** module enables Admin to define, monitor, and enforce **Turnaround Time +(TAT)** for every activity across the dealer lifecycle (onboarding, interviews, FDD, legal, payments, +resignation, F&F, LOI/LOA, EOR, etc.). The system supports **three-level escalation** , **pre-TAT +reminders** , and **post-TAT breach notifications**. All reminders and escalations are **auto-logged in +Work Notes** and **trigger email + in-app notifications** , ensuring traceability, transparency, and +timely closure. + +**9.4.2 Width** + +- **SLA Templates** + o Activity/Stage (e.g., _Level-1 Interview Feedback_ , _FDD Report Upload_ , _Payment_ + _Verification_ , _LOI Approval_ , _Resignation Review_ ). + + +``` +o Owner Role (e.g., DD-ZM , RBM , ZBH , DD-Lead , Finance , Legal , FDD ). +o TAT Unit & Calendar: hours/days, working days +o Pre-TAT Reminders: schedule one or more reminders (e.g., T-48h , T-24h , T-2h ). +o Escalation Matrix (3 levels): +โ–ช L1: After breach +X hours โ†’ Escalate to immediate supervisor (e.g., RBM +โ†’ DD-ZM). +โ–ช L2: If still open +Y hours โ†’ Escalate to zonal authority (e.g., ZBH / DD-Lead). +โ–ช L3: If still open +Z hours โ†’ Escalate to national authority (e.g., DD-Head / +NBH ). +o Notification Channels: email, in-app notification, optional SMS. +o Work Notes Posting: auto-post reminder/escalation entries with timestamp, SLA +name, and due metrics. +o Repeat Overdue Reminders: configurable cadence (e.g., every 24h until closure). +o Pause Rules (optional): pause SLA when status is On Hold / Waiting for +Applicant / Awaiting External (e.g., FDD). +o Scope Rules: by Zone/Region/Area, by Role, by Activity Type, and by Application +Category. +``` +- **Dashboards & Views** + o **My SLA Queue:** due soon, breached, and escalated items for the logged-in user. + o **Aging Buckets:** 0 โ€“ 25%, 26โ€“75%, 76โ€“99%, Breached. + o **SLA Badges** on list cards and detail pages (green/amber/red) with remaining time. + o **Reports:** breach rate, average resolution time, top delayed activities, escalations + by level/role/region. + +**9.4.3 Depth** + +- **Clock Start/Stop Logic** + o SLA starts when the activity is **created/assigned** to the owner role. + o SLA **pauses** on configured statuses (e.g., _Waiting for Applicant / FDD /_ + _Legal_ ), **resumes** on return to active. + o SLA **stops** on closure states (e.g., _Approved/Rejected/Completed_ ). +- **Reminder & Escalation Execution** + o At each pre-TAT checkpoint the system: + โ–ช Sends **email + in-app reminder** to the activity owner. + โ–ช **Posts an automated Work Note** (e.g., โ€œT-24h: Reminder sent to RBM for + Level-1 Feedbackโ€). + o On TAT breach: + โ–ช Marks item **Breached (red)** , posts **Work Note** with elapsed time. + โ–ช Triggers **Escalation L1** to the mapped role; if not resolved within L1 + window, cascades to **L2** then **L3**. + โ–ช Each escalation includes assignee, timestamp, reason, and a link to the + record. + + +**9.4.4 Personas-Wise Accessibility & Visibility** + +``` +Persona Accessibility Visibility +System Admin Create/edit/activate/deactivate SLA templates; +define calendars, holidays, pause rules; set +escalation roles and notification schedules. +``` +``` +Global. +``` +``` +DD-Admin Map activities to templates; monitor SLA status; +initiate corrective routing. +``` +``` +National/regional +(as allowed). +Owners (DD-AM, +RBM, DD-ZM, ZBH, +DD-Lead, Finance, +Legal, FDD) +``` +``` +Receive reminders; act on assigned items; view +timers, badges, and Work Notes; acknowledge +escalations. +``` +``` +Assigned records +and queues. +``` +``` +DD-Head / NBH Receive L3 escalations; view breach dashboards; +intervene/realign ownership. +``` +``` +Pan-India. +``` +``` +System (Automation) Runs timers, posts Work Notes, sends +notifications, cascades L1โ†’L2โ†’L3 escalations, +updates dashboards. +``` +``` +Background. +``` +**9.4.5 Example SLA (illustrative)** + +- **Activity:** _Level-1 Interview Feedback (KT Matrix)_ +- **Owner: DD-ZM + RBM** +- **TAT:** 2 working days; Business hours 9:00โ€“18:00; weekends excluded. +- **Reminders:** T-24h and T-4h to owners. +- **Escalations:** + o **L1 (T+4h):** to **ZBH** + o **L2 (T+12h):** to **DD-Lead** + o **L3 (T+24h):** to **DD-Head/NBH** +- **System Actions:** badges on record; Work Notes for every reminder/escalation; email + in- + app notifications at each step. + + +### 9.5 Email & Letter Templates Management + +**9.5.1 Functionality Scope** + +The **Email & Letter Templates** module enables system administrators to configure and automate +communication across all dealer lifecycle workflows โ€” including onboarding, interviews, +payments, FDD, approvals, and resignation stages. Each template defines **trigger-based +notifications** , ensuring timely and consistent communication between internal users (DD roles, +RBM, ZBH, etc.) and external applicants. Templates can dynamically pull context-specific details +using **system variables** and can be activated, edited, or versioned at any time. + +This module ensures communication uniformity across regions and roles while +supporting **automation triggers** , **personalized content** , and **multi-channel delivery** (email + in- +app notifications). + + +**9.5.2 2. Width** + +``` +9.5.2.1 Template Management Dashboard +``` +- Displays all active and inactive templates with details such as: + o **Template Name** โ€“ e.g., _Application Received_ , _Interview Scheduled_ , _SLA Breach_ + _Warning_. + o **Subject Line** โ€“ dynamic subject that appears in email notifications. + o **Trigger Event** โ€“ specifies when the email is auto-sent (e.g., on approval, rejection, + or SLA breach). + o **Last Modified Date** โ€“ timestamp of latest changes for version control. + o **Actions** โ€“ _Edit_ , _Duplicate_ , _Delete_. + +``` +9.5.2.2 Add / Edit Template +``` +Each email template includes configurable fields: + +- **Template Name:** Internal label for easy identification (e.g., _Application Approved_ ). +- **Email Subject:** Subject line used for recipients (e.g., _Congratulations! Your Application_ + _Has Been Approved_ ). +- **Trigger Event:** Selects the system action that will initiate the email (dropdown includes): + o On Application Submission + o On Approval + o On Rejection + o Interview Scheduled + o Document Request + o Payment Required + o SLA Breach Warning + o Payment Reminder +- **Template Body:** Rich-text editor (HTML support) with system variable placeholders for + dynamic insertion. +- **Active Template Toggle:** Enables or disables template without deletion. +- **Save / Cancel Buttons:** Commit or discard edits. + + +**9.5.3 3. Depth** + +``` +9.5.3.1 Trigger Mechanism +``` +Each configured template is mapped to an **event listener** within the workflow. When a user or +system action matches the trigger condition, an automated email and in-app notification are +generated and dispatched to the intended recipients. + +For example: + +- When an applicant submits a form โ†’ triggers _Application Received_ template. +- When DD-ZM schedules an interview โ†’ triggers _Interview Scheduled_ template. +- When SLA for an activity nears breach โ†’ triggers _SLA Breach Warning_ template. + +``` +9.5.3.2 Dynamic Data Population +``` +Templates leverage **predefined system variables** to automatically populate relevant data, +ensuring contextual accuracy. Available variables include: + +``` +Variable Description +``` +{{applicant_name}} (^) Applicantโ€™s full name +{{application_id}} (^) Unique application identifier +{{application_date}} Submission date +{{interview_date}} (^) Scheduled interview date +{{interview_time}} (^) Scheduled interview time +{{status}} (^) Current application status +{{reason}} (^) Reason for rejection or remark +{{company_name}} Dealer firm or business entity name +{{location}} (^) Preferred / applied dealership location +{{reviewer_name}} (^) Approver or interviewer name +{{payment_amount}} Amount due or verified +{{due_date}} (^) Payment / response deadline +{{support_email}} (^) Official support or contact email +Variables are replaced dynamically at runtime, ensuring personalized and accurate +communications without manual edits. + + +``` +9.5.3.3 Trigger Linkage & Workflow Integration +``` +- The module is fully integrated with system workflows โ€” **Dealer Onboarding, Interview** + **Evaluation, FDD, Finance, Legal, and Resignation**. +- Templates can be reused across similar workflows and roles, minimizing duplication. +- Each workflow can have multiple templates mapped to distinct sub-events (e.g., _Interview_ + _Scheduled_ vs _Interview Rescheduled_ ). + +``` +9.5.3.4 Escalation & SLA Communication Integration +``` +- SLA reminders and escalations leverage the same template framework. +- Templates like _SLA Breach Warning_ and _Pending Action Reminder_ automatically pull + escalation hierarchy and timestamps. +- Escalations are simultaneously logged in **Work Notes** to maintain an auditable + communication trail. + +**9.5.4 4. Personas & Permissions** + +``` +Role Access Type Description +System Admin / DD-Admin Full Access Create, edit, activate, deactivate, or delete +templates; map triggers; modify variables. +DD-Lead / ZBH / DD-Head Limited +View +``` +``` +Can preview active templates relevant to their +workflow. +All Other Roles (RBM, DD- +ZM, Finance, FDD, Legal) +``` +``` +Execution- +Only +``` +``` +Receive or trigger templates automatically; no +edit rights. +``` +**9.5.5 5. Example Template Configuration** + +``` +Field Value +Template +Name +``` +``` +Interview Scheduled +``` +``` +Subject Interview Scheduled โ€“ Royal Enfield Dealership +Trigger When Interview Scheduled +Body Dear {{applicant_name}}, +``` +``` +Your interview for the Royal Enfield Dealership has been scheduled +on {{interview_date}} at {{interview_time}}. +``` +``` +Location: {{location}} +Reviewer: {{reviewer_name}} +``` + +``` +Please ensure timely attendance. +``` +``` +Regards, +Royal Enfield Dealer Development Team +Active Yes +``` +### 9.6 Opportunity Management (Geography & Window Setup) + +**9.6.1 Functionality Scope** + +The **Opportunity Management** module allows Admin to define where and when dealership +opportunities are open. Admin can create opportunities at **Zone โ†’ Region โ†’ Area** granularity, +specify **From / To dates** , and manage the status ( **Active / Inactive / Closed** ). The module also + + +provides **date-range filters** and reports to view **historical opportunity windows** , ensuring +transparency, traceability, and controlled intake of applications. + +**9.6.2 Width** + +- **Create / Edit Opportunity** + 1. **Geography:** Zone โ†’ Region โ†’ Area (cascading drop-downs), plus **State / City /** + **District**. + 2. **Opportunity Details:** Opportunity Type (Main / Studio / Service), **Capacity** (no. of + dealer slots), **Priority** , Notes/Justification. + 3. **Open Window: From Date** and **To Date** (business calendar), optional **Auto-close** + **on end date**. + 4. **Ownership:** Responsible Role (e.g., DD-ZM / RBM) for visibility and SLA routing. + 5. **Status:** Draft / Active / Inactive / Closed. + 6. **Attachments (optional):** Market study, demand assessment, approvals. +- **List & Search** + 1. Columns: State, City, District, Zone, Region, Area, Opportunity Type, Capacity, + Status, Open From, Open To, Last Updated. + 2. Quick actions: **Edit** + 3. Global search and multi-facet filters (Zone/Region/Area, State/City/District, Type, + Status). +- **Date-Range & Historical View** + 1. **Filter by Fromโ€“To Date** to see which locations were open within a selected + window. + 2. Toggle **โ€œShow only open during rangeโ€** or **โ€œShow all with overlapโ€**. + 3. Export results (CSV/XLS) for audits and leadership review. + +**9.6.3 Depth** + +1. **Cascading Geography:** Selecting a **Zone** filters **Regions** ; selecting + a **Region** filters **Areas** ; **State/City/District** lists are bound to the chosen Area. +2. **Window Logic:** An opportunity is **Active** only within its **Fromโ€“To** dates; the system auto- + marks **Closed** on expiry if _Auto-close_ is enabled. +3. **Status Lifecycle:** + o **Draft โ†’ Active โ†’ Inactive/Closed**. Inactive hides the location from the public + form; Closed retains history. +4. **Notifications:** When a window is **activated** or **closed** , notify mapped **DD-ZM/RBM** +5. **Historical Reporting:** + o Date filter computes **effective windows** (open or overlapping) within the selected + range and shows **who created/edited** , timestamps, and notes. + + +**9.6.4 Personas-wise Accessibility & Visibility** + +``` +Persona Accessibility Visibility +Admin / DD- +Admin +``` +``` +Create, edit, activate/deactivate, archive opportunities; +bulk upload; run exports; manage conflicts and capacity. +``` +``` +Nationwide. +``` +``` +DD-Lead / DD- +Head / NBH +``` +``` +View dashboards and historical reports; download +exports. Nationwide.^ +ZBH / DD-ZM / +RBM +``` +``` +View opportunities for their Zone/Region ; receive +activation/closure notifications; capacity view. +``` +``` +Scoped to assigned +geographies. +ASM / DD-AM Read-only list for assigned Areas to plan ground activities. Area-level. +System (Public +Apply Form) +``` +``` +Shows only Active opportunities within current date and +capacity; hides inactive/closed ones. N/A^ +``` +**9.6.5 Validation & UX Notes** + +1. **Required:** Zone, Region, Area, Opportunity Type, From Date, To Date, Status. +2. **Date Checks:** _From_ must be โ‰ค _To_ ; warn if window is in the past; prevent zero-length + windows unless explicitly allowed. +3. **Timezone & Calendar:** Respect business calendar; holidays can be referenced for SLA tie- + ins. +4. **Inline Status Chips: Active (green)** , **Inactive (gray)** , **Closed (blue)** for quick scanning in the + list. +5. **Filter Presets:** _Currently Open_ , _Upcoming_ , _Expired_ , _My Zone_ for fast navigation. + +## 10 F&F Case + +The **Full & Final (F&F) Settlement** process enables the Finance team to close all financial + +obligations with a dealer after resignation or termination. Once triggered by Legal, the +system consolidates inputs from all departments to capture dues, recoveries, and + +clearances. Finance reviews and validates these entries, prepares the final settlement +summary, and executes payment or recovery based on the calculated net amount. All + +actions, remarks, and proofs are recorded in the system for transparency, and the case +is marked as **F&F Completed** once the transaction and approvals are finalized. + + +### 10.1 F&F Settlement Progress Timeline + +**10.1.1 Functionality Scope** + +The **F&F Settlement Progress Timeline** provides a sequential, stage-wise overview of the +dealerโ€™s Full & Final (F&F) settlement journey โ€” right from initiation to final completion. +It acts as a unified visual tracker for Finance, Legal, DD, and Admin teams, enabling transparent +monitoring of all financial closure activities, departmental dependencies, dealer discussions, +and documentation milestones. + +Each stage dynamically updates in real-time based on workflow actions performed by +responsible stakeholders, showing the exact case status and progress across all involved +departments. + + +**10.1.2 Width** + +The timeline integrates all key phases and users involved in the financial closure ecosystem, +including: + +- **DD-Lead / DD-Admin:** Initiate the F&F process upon Legal approval of Resignation or + Termination. +- **Finance:** Validate departmental responses, calculate payables/recoverables, initiate + discussion with the dealer, and finalize settlement disbursement or recovery. +- **Departments (16 Functional Units):** Submit financial clearances or pending dues data + through their respective interfaces. +- **Legal:** Verify settlement completion for compliance and record-keeping. + +**10.1.3 Depth** + +The timeline comprises six structured stages, each with clearly defined ownership, system +actions, and dependencies. + +``` +10.1.3.1 F&F Initiated +``` +- **Owner:** DD-Lead / DD-Admin +- **Description:** + Marks the creation of the F&F case post-approval of Resignation or Termination. + System auto-generates the **Case Number** (e.g., _FNF- 2025 - 001_ ) and pre-populates dealer + details such as name, location, and request type. +- **System Actions:** + o Case record created under Finance module. + o Notification sent to Finance and departmental stakeholders. + o Status: _Completed_ once initialization is confirmed. + +``` +10.1.3.2 Department Responses Received +``` +- **Owner:** All Functional Departments +- **Description:** + Each department submits its NOC or dues-related information through the integrated + F&F clearance form. + Departments that owe or are owed amounts mark respective payables/receivables with + remarks. +- **System Actions:** + + +``` +o Progress bar updates with response count (e.g., 12 of 16 Departments +Responded ). +o SLA-based reminders triggered for pending responses. +o Timeline stage remains Pending until all NOCs are received or escalated. +``` +``` +10.1.3.3 Finance Final Summary +``` +- **Owner:** Finance +- **Description:** + The Finance team consolidates all departmental responses, computes total payables, + receivables, and deductions, and prepares a comprehensive **Settlement Summary** + **Report**. +- **System Actions:** + o Auto-calculation using predefined formula: + Net Settlement = Total Payables โ€“ Total Receivables โ€“ Deductions. + o Finance reviews and verifies supporting documents. + o Work Notes used to raise clarifications to departments or DD-Lead. + o Status changes to _Pending Dealer Discussion_ after internal approval. + +``` +10.1.3.4 Financial Discussion with Dealer +``` +- **Owner:** Finance + Legal + DD-Lead +- **Description:** + The Finance and Legal teams review the computed summary with the dealer to confirm + payable or recoverable balances. + Dealer may be invited to review supporting documentation and validate accuracy. +- **System Actions:** + o Discussion details logged under **Work Notes** with date and participants. + o Dealer confirmation captured in remarks. + o Settlement sheet locked for final processing once dealer agreement is + confirmed. + +``` +10.1.3.5 Full and Final Settlement +``` +- **Owner:** Finance +- **Description:** + All financial actions โ€” including payments, recoveries, and internal ledger updates โ€” + are executed. + Proof of payment, transaction IDs, and settlement receipts are uploaded. +- **System Actions:** + o Transaction details (Mode, Reference, Amount, Date) entered in **Settlement** + **Verification**. + + +``` +o Status updated to Processed once Finance approves the settlement. +o System triggers automated notifications to DD-Admin, Legal, and DD-Lead. +``` +``` +10.1.3.6 F&F Complete +``` +- **Owner:** Finance + DD-Admin + Legal +- **Description:** + The final stage confirming that the F&F process has been fully completed, all payments + or recoveries are reconciled, and all documentation is finalized. +- **System Actions:** + o Case status updated to _Closed_. + o Settlement report archived in **Audit Trail**. + o Final closure notification sent to all stakeholders. + +**10.1.4 Personas-wise Accessibility & Visibility** + +``` +Persona Timeline Visibility Actions Allowed +DD-Lead / DD- +Admin +``` +``` +Full visibility of all stages from initiation to +completion. +``` +``` +Initiate F&F, Upload +Docs, Add Notes +Finance Complete visibility across all stages with +actionable control from Stage 3 onwards. +``` +``` +Verify, Approve, Reject, +Comment +Departments (16 +Units) +``` +``` +Visible until Department Responses stage. Submit NOC, Add +Comments +Legal Visible from Dealer Discussion to Final +Closure. +``` +``` +Review, Comment +``` +``` +NBH / ZBH / DD- +Head +``` +``` +View-only summary of financial progress. None +``` + +### 10.2 Department Responses + +**10.2.1 Functionality Scope** + +The **Department Responses** section serves as a consolidated interface for tracking NOC +submissions and financial dues from all departments involved in the dealerโ€™s Full & Final (F&F) +settlement. +It provides Finance and DD teams with a transparent view of each departmentโ€™s clearance +status, whether the department owes a payment to the dealer ( _Payable_ ) or the dealer owes the +department ( _Recovery_ ). +This enables complete financial visibility before the final settlement summary is prepared. + +**10.2.2 Width** + +This module connects all **functional departments (up to 16 units)** including Sales, Service, Parts, +Finance, Warranty, Marketing, HR, IT, Legal, Logistics, and Quality. +Each department inputs its clearance data โ€” marking whether any dues exist โ€” and provides +supporting remarks or payable/recovery amounts. +The respective department person will login and fill his respective amount. + +**10.2.3 Depth** + +- **Status Indicators:** + Each departmentโ€™s submission is color-coded and categorized as: + o ๐Ÿ”ด _Dues_ โ€“ Outstanding amount identified. + + +``` +o ๐ŸŸข No Dues โ€“ Cleared with no financial impact. +o โšช Pending โ€“ Awaiting departmental response or review. +``` +- **Amount Details:** + When dues are identified, the department specifies the **Amount Type** (Payable or + Recovery) and corresponding **Value** , which directly contributes to the Finance teamโ€™s + final calculation matrix. +- They will login with there respective account and fill the details. +- **Remarks Section:** + Every response includes contextual remarks for clarity, such as โ€œOutstanding amount + identifiedโ€ or โ€œCleared,โ€ ensuring traceable communication between departments and + Finance. + +**10.2.4 Personas-wise Accessibility & Visibility** + +``` +Persona Role in this Section Access Level +Finance Reviews all departmental submissions, verifies +payable/recovery entries, adds notes. +``` +``` +Full Access +``` +``` +Departments (16 +Units) +``` +``` +Submit NOC, mark dues/no-dues, enter remarks, and +upload proofs if applicable and add amount (if any) +``` +``` +Edit / Submit +``` +``` +DD-Lead / DD- +Admin +``` +``` +Monitors overall progress of departmental responses and +follows up on pending inputs. +``` +``` +View / +Comment +Legal / NBH / ZBH Verify final status before case closure. View Only +``` +## 11 Finance Dashboard + +The **Finance Dashboard** provides a unified workspace for managing all financial activities +related to dealer onboarding and offboarding. It gives Finance users complete visibility + +into **pending verifications, approved transactions, and Full & Final (F&F) settlements** across +both Resignation and Termination cases. The dashboard is divided into two key + +segments โ€” **Onboarding** , which focuses on verifying dealer security deposits and initial +payments made via RTGS or NEFT, and **F&F Settlement** , which consolidates all + +department-wise responses, calculates final payable or recoverable amounts, and +facilitates settlement approvals. + + +### 11.1 Finance Dashboard Page + + +**11.1.1 Functionality Scope** + +The **Finance Dashboard** serves as the centralized workspace for the Finance team to verify +dealer-related financial transactions and settlements โ€” both during onboarding and +offboarding processes. +It ensures end-to-end visibility of **Security Deposit verifications** for new dealerships and **Final +F&F settlements** for dealers resigning or terminated, thereby providing financial traceability +across the dealership lifecycle. + +The dashboard operates in two distinct functional tabs: + +- **Onboarding:** For validating advance payments (Security Deposit, Initial Fees, etc.) + submitted by dealers during application onboarding. +- **F&F Settlement:** For managing Final Settlement workflows + upon **Resignation** or **Termination** , involving multi-department inputs and Finance + validation before closure. + +The system provides summarized counters for quick insights โ€” _Pending Verification_ , _Verified +Payments_ , _Pending F&F Summaries_ , and _Completed F&F_ โ€” enabling Finance to prioritize action +items efficiently. + +**11.1.2 Width** + +The Finance Dashboard is cross-functional, connecting the following stages and roles: + +- **During Onboarding:** + o Receives dealer payment data (Security Deposit, Bank Details, Transaction ID, + Mode of Payment, etc.). + o Enables Finance users to verify authenticity of RTGS/NEFT transactions by cross- + checking with corporate account statements. + o Allows upload of verified transaction proof or remarks in case of mismatch. +- **During Offboarding (Resignation / Termination):** + o Auto-fetches the list of dealers approved for exit by NBH and Legal. + o Tracks the **F&F Summary** preparation status and department responses. + o Consolidates financial liabilities, recoverables, or pending clearances. + o Generates a unified view of financial closure and triggers completion once all + departments respond. + +The dashboard integrates with **Legal** , **DD-Admin** , and **DD-Lead** modules to ensure that once a +dealer exit is approved, the Finance team receives all relevant data automatically for settlement +initiation. + + +**11.1.3 Depth** + +``` +11.1.3.1 Onboarding โ€“ Payment Verification +``` +- **Initiation:** + Dealer payment details (Security Deposit, Mode of Payment, Transaction ID, and Bank + Name) are captured during onboarding. +- **Verification Process:** + o Finance validates the transaction against company account records. + o Uploaded documents like **Payment Receipt** or **Bank Statement** are reviewed. + o Finance user confirms verification by entering the verified transaction ID, + received date, and remarks. +- **System Actions:** + o On successful verification, payment status updates to **Verified** , triggering an + email + in-app notification to DD-Admin and DD-Lead. + o If discrepancies are found, Finance can flag the payment for review with remarks + in Work Notes. +- **Dashboard Counters:** + o **Pending Verification:** Lists all onboarding payments awaiting Finance + confirmation. + o **Verified:** Displays successfully validated payments along with transaction logs + and verifier details. + +``` +11.1.3.2 Offboarding โ€“ F&F Settlement Summary +``` +- **Trigger:** + Once Legal uploads the **Resignation Acceptance** or **Termination Letter** , the case + automatically appears in the Finance Dashboard under _F&F Settlement._ +- **Process Flow:** + 1. System collates the **Dealer Exit Case (Resignation/Termination)** details. + 2. Pulls financial obligations, pending dues, recoverables, and credit balances from + connected departments (e.g., Parts, Apparel, DMS, Marketing). + 3. Displays a departmental response tracker (e.g., _16/16 Departments Responded_ ). + 4. Finance reviews the consolidated data and creates the **Final Settlement** + **Summary**. + 5. On approval, status changes from _Pending Finance Summary_ to _Completed_ and + the record is archived for reporting. +- **Work Note & Communication:** + 1. Finance can use the **Work Notes** tab to tag DD-Lead, Legal, or Admin in case + clarifications are needed. + 2. Each note gets timestamped and appears under **Audit Trail** for traceability. + + +3. Upon finalization, a system-generated confirmation triggers notification to DD- + Admin for closure. +- **Automation & Notifications:** +1. SLA reminders alert Finance for pending verifications nearing expiry. +2. Status changes (Pending โ†’ Verified / Completed) are reflected across modules +instantly. + +**11.1.4 Personas-wise Accessibility & Visibility** + +``` +Persona Responsibilities & Actions Access Level +``` +``` +Finance +(Primary +Owner) +``` +``` +Verify onboarding payments, review RTGS details, create and +approve F&F summaries, add Work Notes. Full Access^ +``` +``` +DD-Admin Upload payment proofs during onboarding, upload dealer reply or +Legal letters during offboarding, view Finance remarks. +``` +``` +Upload / +View +``` +``` +DD-Lead +``` +``` +Review verified payment records, view F&F progress, respond to +Finance queries in Work Notes. +``` +``` +View / +Comment +Legal Cross-reference Finance completion before case closure. View Only +``` +``` +NBH / ZBH Monitor high-level financial progress for terminated or resigned +dealers. +``` +``` +View Only +``` +``` +Dealer +(Read-only) +``` +``` +Can view payment verification and F&F closure confirmation in +dealer portal. Once a dealer resigns or is terminated , portal access +is permanently revoked , preventing any further system interaction. +``` +``` +View Only +``` + +### 11.2 F&F Settlement Module + + + +**11.2.1 Functionality Scope** + +The **Full & Final (F&F) Settlement module** enables Royal Enfieldโ€™s Finance division to execute, +validate, and document the final financial closure of any dealer account +following **Resignation** or **Termination** approval. +It consolidates all monetary data โ€” payables, receivables, deductions, and department-wise +clearances โ€” into a unified interface for transparent and compliant settlement processing. + +The module provides a structured workflow that ensures all dependencies are cleared across +departments, settlement calculations are system-validated, and final payouts or recoveries are +accurately recorded with bank transaction details. +The process is fully integrated with Legal, Dealer Development (DD), and Admin workflows, +ensuring that once a dealer exit is approved, the F&F process is automatically triggered within +defined SLAs. + +**11.2.2 Width** + +The F&F module covers both **Resignation** and **Termination** closure workflows, integrating all +stakeholders and systems that influence the final settlement outcome. + +- **Dealer Development (DD-Lead / DD-Admin):** + Triggers F&F process after Legal uploads the acceptance or termination letter. +- **Finance:** + Leads the overall settlement process, validates departmental inputs, performs + reconciliation, and confirms final payment or recovery transactions. +- **Departments (16 Functions):** + Submit NOC and financial inputs through automated task prompts (e.g., Parts, Service, + Apparel, HR, Legal, Quality, Marketing, IT, Logistics, etc.). +- **Legal:** + Verifies F&F completion before case closure and maintains compliance documentation. +- **Admin:** + Uploads settlement proof and coordinates with Finance for record finalization. + +This ensures that no dealer account is financially closed until all clearances, proofs, and +validations are in place. + + +**11.2.3 Depth** + +``` +11.2.3.1 Case Overview and Summary +``` +Each F&F case is system-generated with a unique ID (e.g., _FNF- 2024 - 001_ ). +Key case metadata displayed includes: + +- Dealer name, code, and location +- Termination type (Resignation / Termination) +- Submitted and due dates +- Associated domain and sales/service codes +- Case age and current status ( _Pending Finance Review_ , _Completed_ ) + +A **Net Payable / Receivable Indicator** at the top visually represents whether the company owes +payment to the dealer or vice versa. +For example: _Payable to Dealer โ€“ โ‚น9,75,000_ indicates a net payout scenario after adjustments. + +``` +11.2.3.2 Department-wise Clearance Tracking +``` +This section provides a real-time tracker of department responses and clearances. +It includes: + +- **Progress Bar:** Displays total responses received vs. pending (e.g., _12 of 16 departments_ + _responded_ ). +- **NOC Statuses:** + o _NOC Submitted_ โ€“ Department confirms zero dues. + o _Dues Pending_ โ€“ Department flags financial obligations. + o _Pending_ โ€“ Awaiting department review. +- **Response Details Table:** Lists each department with submitted date, clearance remarks, + and any recovery or payable amount. +- **Response Guidelines Panel:** Summarizes submission protocols and auto-reminder SLAs. + +Departments with dues or recovery inputs automatically impact the **Receivable / Deduction +Summary** under Finance Calculation. + +``` +11.2.3.3 Financial Calculation Summary +``` +Finance users can view, verify, and edit financial items categorized into **three structured +sections:** + + +``` +11.2.3.4 Payables to Dealer (Editable) +``` +Represents refundable amounts due from the company to the dealer, such as: + +- Security Deposit refund +- Inventory valuation +- Equipment and fixture reimbursements +- Outstanding credit notes + +Finance users can add new line items with department tags and descriptions. +Each editable record auto-calculates into the total payables panel. + +``` +11.2.3.5 Receivables from Dealer (Editable) +``` +Captures outstanding recoverables and pending dues, including: + +- Outstanding invoices (Sales / Parts / Service) +- Marketing recoveries +- HR or Finance advances +- Compliance or penalty adjustments + Each record can be added, edited, or deleted before final review. + +``` +11.2.3.6 Deductions (Editable) +``` +Represents contingent deductions such as: + +- Pending warranty claims +- Policy violations +- Miscellaneous settlements + +Each itemโ€™s description, department, and value feed into the **Total Deductions** summary. + +``` +11.2.3.7 System-Calculated Formula +``` +At the bottom, a dynamic calculation displays: + +``` +Net Settlement = Total Payables โ€“ Total Receivables โ€“ Total Deductions +``` +A positive balance indicates _Payable to Dealer_ ; a negative balance indicates _Recovery from +Dealer_. + + +``` +11.2.3.8 Settlement Verification Panel +``` +Located on the right side, this panel captures the **final transaction details** once the Finance +review is complete. + +Fields include: + +- **Payment Mode:** NEFT / RTGS / Cheque +- **Transaction / Reference ID:** Corporate transaction number +- **Bank Reference Number:** Optional for verification +- **Settlement Amount & Adjustments:** Auto-fetched from calculation summary +- **Settlement Date:** Date of transfer or adjustment posting +- **Verification Remarks:** For audit or cross-team comments + +Finance can then take one of three workflow actions: + +- **Approve Settlement:** Marks case as โ€œFinance Approved.โ€ +- **Request Clarification:** Sends query back to DD-Lead or Admin with remarks. +- **Reject Settlement:** Moves case to โ€œReturned for Correctionโ€ with detailed reason. + +Each action automatically logs under **Audit Trail** and triggers email + system notifications. + +``` +11.2.3.9 Documents Section +``` +This tab centralizes all artefacts submitted or generated during the F&F process. + +It includes: + +- Dealer documents (e.g., _Resignation Letter_ , _Asset Handover Receipt_ , _Inventory_ + _Report_ , _Bank Statement_ ). +- Uploaded proofs by Finance (e.g., _Settlement Proof, Payment Receipt_ ). +- Legal or DD attachments for traceability. + +A **drag-and-drop upload zone** allows Finance or Admin to attach additional records (PDF, DOC, +XLSX, JPG) up to 10 MB each. +Each file is logged with: + +- File name and type +- Upload date and user +- Download option for audit access + + +``` +11.2.3.10 Bank Details Tab +``` +Displays dealer bank information to validate payment transfer: + +- Account holder name +- Account number +- IFSC and branch name +- Bank name + +A system alert prompts the verifier to validate details before disbursing payment: +_โ€œBank Verification Required โ€“ Please confirm bank account before processing settlement.โ€_ + +``` +11.2.3.11 Settlement Checklist +``` +A final control checklist ensures financial compliance before marking the case as complete. It +includes mandatory checks for: + +- Verification of all financial calculations +- Confirmation of bank account details +- Review of all department responses +- Upload of settlement proof +- Entry of accurate transaction information + +All checklist points must be validated before the โ€œApprove Settlementโ€ button becomes active. + +**11.2.4 Work Notes & Communication Flow** + +- Every clarification, remark, or inter-team discussion is captured through the **Work** + **Note** feature integrated into the F&F module. +- Finance, DD-Lead, and Legal can tag specific users (e.g., _@Admin_ , _@Legal_ ) to address + pending actions. +- Notes are timestamped and visible in the case timeline. +- Work Notes become part of the permanent **Audit Trail** and ensure transparent + communication without relying on emails. + +**11.2.5 Personas-wise Accessibility & Visibility** + +``` +Persona Responsibilities Access +Rights +``` + +``` +Finance (Primary +Owner) +``` +``` +Review, calculate, and approve final settlements; update +transaction details; upload settlement proof; +communicate via Work Notes. +``` +``` +Full Access +``` +``` +DD-Admin Upload dealer responses, asset handover, and supporting +docs; coordinate with Finance for closure. +``` +``` +Upload / +View +DD-Lead Review and confirm financial summaries; respond to +clarifications. +``` +``` +Review / +Comment +Legal Validate compliance and verify settlement proof before +closure. +``` +``` +View / +Comment +Departments (16 +Units) +``` +``` +Submit NOC, recovery, or clearance via linked tasks. Limited Edit +Access +NBH / ZBH / DD- +Head +``` +``` +Monitor overall settlement status and amount trends. View Only +``` +``` +Dealer (Read- +only) +``` +``` +View F&F confirmation and settlement proof post-closure. View Only +``` +## 12 Dealer Persona + +The Dealer Self-Service module empowers **onboarded dealers** with controlled, role-based access +to initiate and track key post-onboarding lifecycle requests through a unified portal. Dealers are +enabled to **initiate dealership resignation, request relocation to a new location, and submit +constitutional change requests** , each governed by structured workflows, mandatory +documentation, and defined approval hierarchies. + +### 12.1 Dealer Resignation + + + +**12.1.1 Functionality Scope** + +This functionality enables a **dealer to initiate, track, and manage dealership resignation +requests** through the portal after successful onboarding. The system provides a **guided +resignation submission experience** , outlet-level visibility, and a transparent approval journey +supported by **Work Notesโ€“based communication, audit logging, and role-based governance +controls**. Dealers are permitted to **withdraw a resignation request only until the case is pending +with the National Business Head (NBH)**. + +**12.1.2 Functional Width** + +- Displays a **dealer-facing resignation dashboard** with summary indicators: + o Total Outlets + o Active Outlets + + +``` +o Pending Resignations +``` +- Lists all **dealer-owned outlets** with: + o Outlet name and code + o Address and city + o Establishment date + o Current operational status +- Enables **outlet-level resignation initiation** +- Prevents **multiple resignation requests** for the same outlet +- Displays **โ€œResignation in Progress โ€“ View Requestโ€** when a request already exists +- Provides a **detailed resignation view** with: + o Request details + o Operational and commercial information + o Uploaded documents + o Progress timeline + o Audit trail +- Allows **resignation withdrawal** based on workflow stage eligibility +- Displays **informational guidance** related to F&F settlement and departmental clearances + +**12.1.3 Functional Depth** + +- The system allows resignation initiation **only for active and eligible outlets**. +- On selecting **โ€œRequest Resignationโ€** , the dealer is presented with a structured submission + form capturing: + o Resignation type + o Last Operational Date โ€“ Sales + o Last Operational Date โ€“ Services + o Reason for resignation + o Optional additional information +- Outlet information (code, name, type, city, address) is **auto-populated and non-editable**. +- The **Last Working Day (LWD)** entered during submission is stored as the **authoritative** + **reference date** for downstream processing. +- Upon submission, the resignation request progresses through a **multi-level approval** + **workflow** , typically involving: + o DD ASM + o DD ZM / RBM + o ZBH + o DD Lead + o DD Head + o NBH + o Legal +- Each stage is reflected in a **visual progress timeline** , including: + o Stage status + + +``` +o Action date +o Uploaded document count +``` +- Authorized internal users can **Approve, Send Back, or Revoke** the resignation request. +- **Send Back and Revoke actions are communicated exclusively through Work Notes** , + with **mandatory remarks** captured by the system. +- The **dealer is allowed to withdraw the resignation request only until the case is pending** + **with NBH**. +- Once the resignation request **moves beyond the NBH stage** , the **withdrawal option is** + **disabled** by the system. +- Withdrawal actions are: + o Time-stamped + o Logged in the audit trail + o Communicated to relevant internal stakeholders +- Upon completion of approvals, the **Legal team issues the official Resignation Acceptance** + **Letter**. +- The **Full & Final (F&F) settlement process is triggered strictly on the Last Working Day** + **(LWD)** and **not based on the approval date**. +- After resignation closure: + o The outlet is marked as closed + o **Dealer portal access is revoked** as per access control policy +- All actions, documents, remarks, and transitions are **captured for audit and compliance** + **purposes**. + +**12.1.4 Personas-wise Accessibility & Visibility** + +``` +Persona Responsibilities Access Rights +Dealer Initiates and tracks resignation requests +for owned outlets. +``` +- View outlet list and resignation +status +- Initiate resignation request +- Submit resignation details and +documents +- View progress timeline, Work Notes, +and audit trail +- **Withdraw resignation request only +until the case is pending with NBH** +- No approval, send back +DD ASM Supports coordination and +documentation during resignation +processing. +- View resignation requests +- Upload supporting documents +- Participate in coordination activities +DD ZM Performs zonal-level review and +validation. +- View resignation requests +- Review and provide inputs + + +``` +RBM Performs regional business review. โ€ข View resignation requests +``` +- Review and recommend +ZBH Ensures zonal governance and decision +alignment. +- Review resignation requests +- Send Back or Revoke with +mandatory Work Notes +- Approve as per hierarchy +DD Lead Ensures process adherence and cross- +functional alignment. +- Review resignation requests +- Send Back or Revoke with +mandatory Work Notes +- Approval visibility +DD Head Oversees dealer development +governance. +- Review resignation requests +- Send Back or Revoke with +mandatory Work Notes +- Approve as per hierarchy +NBH Provides senior business oversight. โ€ข Review resignation requests +- Send Back or Revoke with +mandatory Work Notes +- Final approval authority +Legal +Team + +``` +Issues formal resignation closure +documentation. +``` +- Issue Resignation Acceptance Letter +- View complete resignation details +System Enforces workflow rules and +compliance. +- Control action availability +- Trigger notifications +- Initiate F&F on LWD +- Maintain audit trail + +### 12.2 Dealer Constitutional Change Management + + +**12.2.1 Functionality Scope** + +This functionality enables a **dealer to initiate, track, and manage requests for change in business +constitution** through the portal after successful onboarding. The system provides a **structured +self-service mechanism** to propose constitution changes, capture legally required information, +and submit **constitution-specific mandatory documents** , while routing the request through +a **defined internal review and approval workflow**. + +**12.2.2 Functional Width** + +- Displays a **dealer-facing constitutional change dashboard** with summary indicators: + o Total Requests + o Pending Requests + + +``` +o Completed Requests +``` +- Lists all **constitution change requests** with: + o Request ID + o Current constitution + o Proposed constitution + o Submission date + o Current status + o Progress percentage +- Enables **initiation of a new constitutional change request** +- Supports the following **constitution change cases** : + o Proprietorship (Single Owner) โ†’ Partnership + o Proprietorship โ†’ LLP (Limited Liability Partnership) + o Proprietorship โ†’ Private Limited + o Partnership โ†’ LLP + o Partnership โ†’ Private Limited + o Private Limited โ†’ LLP + o Private Limited โ†’ Partnership +- Dynamically determines **mandatory document requirements** based on the **target** + **constitution** +- Allows **document upload only as per applicable case** +- Provides **role-based visibility** into request details, documents, and progress +- Prevents duplicate or parallel requests as per policy + +**12.2.3 Functional Depth** + +- Constitutional change requests can be initiated **only for active and eligible dealers**. +- On selecting **โ€œNew Constitutional Changeโ€** , the dealer is presented with a structured + submission form capturing: + o Dealer Code and Dealer Name (auto-populated, non-editable) + o Current constitution (auto-populated) + o Proposed constitution (selectable from allowed options) + o Reason for change + o Details of new partners / members (where applicable) + o Proposed shareholding pattern +- Based on the **proposed constitution** , the system determines the **mandatory document** + **checklist** as follows: + +**12.2.4 Document Applicability Rules** + +**A. Any change resulting in Partnership requires:** + + +- GST Registration Certificate +- Firm PAN Copy +- Self-attested KYC documents +- Partnership Agreement (Notarised) +- Business Purchase Agreement (BPA) +- Firm Registration Certificate (Partnership) +- Cancelled Cheque +- Declaration / Authorization Letter + +**B. Any change resulting in LLP requires:** + +- GST Registration Certificate +- Firm PAN Copy +- Self-attested KYC documents +- Certificate of Incorporation (COI) +- Business Purchase Agreement (BPA) +- LLP Agreement (Notarised) +- Cancelled Cheque +- Declaration / Authorization Letter + +**C. Any change resulting in Private Limited requires:** + +- GST Registration Certificate +- Firm PAN Copy +- Self-attested KYC documents +- MOA (Memorandum of Association) +- AOA (Articles of Association) +- Certificate of Incorporation (COI) +- Business Purchase Agreement (BPA) +- Cancelled Cheque +- Declaration / Authorization Letter + +**D. Any change resulting in Proprietorship requires:** + +- GST Registration Certificate +- Firm PAN Copy +- Self-attested KYC documents +- Cancelled Cheque +- Declaration / Authorization Letter +- The system enforces **document completeness validation** before allowing submission or + progression. + + +- **No OCR or automated document content extraction** is performed; all validations + are **manual and role-driven**. +- Upon submission: + o The request is routed through a **multi-level internal review workflow** (DD ASM โ†’ + DD ZM / RBM โ†’ ZBH โ†’ DD Lead โ†’ DD Head โ†’ NBH โ†’ Legal, as applicable). +- Authorized internal roles may **Approve, Send Back, or Revoke** the request. +- **Send Back and Revoke actions are communicated through Work Notes** , with mandatory + remarks. +- The **Legal team validates statutory compliance** and facilitates updates to dealer master + records post-approval. +- Upon final approval: + o Dealer constitution details are updated in the system of record. + o All actions, documents, and decisions are **logged for audit and compliance**. + +**12.2.5 11.2.4 Personas-wise Accessibility & Visibility** + +``` +Persona Responsibilities Access Rights +Dealer Initiates and tracks constitutional +change requests. +``` +- Initiate new constitutional change +request +- Provide change details and reasons +- Upload mandatory documents as +per applicable case +- View request status, progress, and +Work Notes +DD ASM Coordinates document collection and +supports validation. +- View requests +- Upload supporting documents +- Assist in coordination +DD ZM Performs zonal-level review and +validation. +- View requests +- Review and provide inputs +RBM Conducts regional business evaluation. โ€ข View requests +- Review and recommend +ZBH Ensures zonal governance compliance. โ€ข Review requests +- Send Back or Revoke with +mandatory Work Notes +- Approve as per hierarchy +DD Lead Ensures adherence to dealer +development policies. +- Review requests +- Send Back or Revoke with +mandatory Work Notes +- Approval visibility + + +``` +DD Head Oversees dealer development +governance. +``` +- Review requests +- Send Back or Revoke with +mandatory Work Notes +- Approve as per hierarchy +NBH Provides senior management approval. โ€ข Review requests +- Send Back or Revoke with +mandatory Work Notes +- Final approval authority +Legal +Team + +``` +Validates statutory compliance and legal +documentation. +``` +- Review documents +- Validate compliance +- Facilitate post-approval updates +System Enforces rules and audit compliance. โ€ข Determine applicable documents +dynamically +- Validate completeness (no OCR) +- Track progress and status +- Maintain audit trail + +Dealer Relocation Request + + + +**12.2.6 Functionality Scope** + +This functionality enables a **dealer to initiate, track, and manage dealership relocation +requests** through the portal after successful onboarding. The system provides a **guided self- +service mechanism** to propose a new dealership location, submit **location-specific statutory, +property, and infrastructure documents** , and route the request through a **multi-level internal +approval workflow**. + +**12.2.7 Functional Width** + +- Displays a **dealer-facing relocation dashboard** with summary indicators: + o Total Requests + o Pending Requests + o Completed Requests +- Lists all **relocation requests** with: + o Request ID + o Current location + o Proposed location + o Distance from current location + o Submission date + o Current status + o Progress percentage +- Enables **initiation of a new relocation request** +- Allows **manual address entry** or **map-based location selection** for the proposed site +- Captures **distance from the existing location** +- Provides **request-level detailed view** including: + o Relocation overview + o Submitted information + o Workflow progress + + +``` +o Required and uploaded documents +o History & audit trail +``` +- Supports **document upload, verification, and status tracking** +- Provides **role-based visibility and action controls** +- Prevents parallel or duplicate relocation requests for the same outlet + +**12.2.8 11.3.3 Functional Depth** + +- Relocation requests can be initiated **only for active and eligible dealerships**. +- On selecting **โ€œNew Relocation Requestโ€** , the dealer is presented with a structured + submission form capturing: + o Dealer Code and Dealer Name (auto-populated, non-editable) + o Current dealership address (auto-populated) + o Proposed new location (manual entry or map selection) + o Complete address details (city, state, pincode) + o Distance from the current location + o Property type + o Expected relocation date + o Reason for relocation +- Upon submission, the request enters a **multi-level approval workflow** , typically + progressing through: + o DD ASM Review + o RBM Review + o DD ZM Review + o ZBH Review + o DD Lead Review + o NBH Review + o Legal (as applicable) +- Each stage is reflected through a **visual workflow progress timeline** , showing: + o Responsible role + o Stage status (Completed / In Progress / Pending) + o Overall progress percentage +- The system enforces **mandatory document submission and verification** , categorized as: + o Property + o Legal + o Statutory + o Infrastructure +- Required documents include, but are not limited to: + o Property documents for new location + o Lease / Rental agreement for new location + o NOC from current landlord + o Municipal approvals + + +``` +o Fire safety certificate +o Pollution clearance +o Layout / Floor plan of new location +o Photos of new location +o Locality map +o Building plan approval +o Electricity connection documents +o Water supply documents +``` +- Document status is tracked as **Pending Verification** , **Verified** , or **Rejected**. +- Authorized internal users may **Approve, Send Back, or Revoke** the relocation request. +- **Send Back and Revoke actions are communicated through Work Notes** , with mandatory + remarks captured by the system. +- All uploads, verifications, remarks, and approvals are **logged in the audit trail**. +- Upon final approval: + o The relocation request is marked as completed. + o Dealer master records are updated as per the approved new location. +- The system ensures **full traceability and compliance** across all stages of the relocation + process. + +**12.2.9 11.3.4 Personas-wise Accessibility & Visibility** + +``` +Persona Responsibilities Access Rights +Dealer Initiates and tracks dealership +relocation requests. +``` +- Initiate relocation request +- Provide proposed location details +- Upload required documents +- View request status, workflow +progress, and Work Notes +DD ASM Coordinates initial review and +document readiness. +- View relocation requests +- Upload and review documents +- Support coordination +RBM Performs regional feasibility and +business review. +- View requests +- Review and recommend +DD ZM Conducts zonal-level evaluation. โ€ข View requests +- Review and provide inputs +ZBH Ensures zonal governance and +compliance. +- Review requests +- Send Back or Revoke with mandatory +Work Notes +- Approve as per hierarchy +DD Lead Ensures policy adherence and cross- +functional alignment. +- Review requests +- Send Back or Revoke with mandatory +Work Notes +- Approval visibility + + +``` +NBH Provides senior management approval. โ€ข Review requests +``` +- Send Back or Revoke with mandatory +Work Notes +- Final approval authority +Legal +Team + +``` +Validates statutory and legal +compliance. +``` +- Review legal documents +- Validate approvals and clearances +System Enforces workflow and compliance +rules. +- Control action availability +- Track document status and progress +- Maintain history and audit trail + +## 13 Non-Functional Requirements + +``` +Category Requirement +Performance Average response time < 3 seconds for standard operations. +Scalability Should scale horizontally on GCP. +Security JWT tokens, encrypted passwords, HTTPS enforced. +Usability Intuitive UI, consistent icons, and simple navigation. +Reliability 99% uptime target. +Backup & Recovery Daily database backup and weekly full snapshot. +Compliance Follows RE IT data privacy guidelines. +``` +## 14 Technology Matrix + +``` +Component Specification +Database PGSQL (Managed or local instance) +Application Stack Node.js (Backend) + React.js (Frontend) +Authentication RE SSO Bridge +``` +## 15 Infra requirements & System Hygiene + +``` +Component Specification +Environment QA / Testing +# of Virtual Machines (VMs) 1 +CPU Configuration 4 - Core +Memory (RAM) 16 GB +Disk Size 500 GB +Operating System Ubuntu 24.04 LTS +``` + +``` +Storage Type Cloud +``` +Backup and Recovery + +- Daily incremental and weekly full backups. +- Restore process must not exceed 2 hours. + +## 16 Not in scope + +Anything which comes beyond the scope defined above in terms of Width and depth + + diff --git a/docs/dealer_onboard_backend_schema.mermaid b/docs/dealer_onboard_backend_schema.mermaid new file mode 100644 index 0000000..6b6f4eb --- /dev/null +++ b/docs/dealer_onboard_backend_schema.mermaid @@ -0,0 +1,1189 @@ +erDiagram + %% ============================================ + %% DEALER ONBOARDING BACKEND SCHEMA + %% Based on Re_New_Dealer_Onboard_TWO.md (v1.4) + %% Comprehensive Database Schema Design + %% ============================================ + + %% ============================================ + %% USER MANAGEMENT & AUTHENTICATION + %% ============================================ + USERS { + uuid user_id PK + string employee_id UK + string email UK + string full_name + string mobile_number + string department + string designation + uuid role_code FK + uuid zone_id FK + uuid region_id FK + uuid state_id FK + uuid district_id FK + uuid area_id FK + uuid dealer_id FK + boolean is_active + boolean is_external + string sso_provider + string status + timestamp last_login + timestamp created_at + timestamp updated_at + } + + ROLES { + uuid role_id PK + string role_code UK + string role_name + string description + string category + boolean is_active + timestamp created_at + timestamp updated_at + } + + PERMISSIONS { + uuid permission_id PK + string permission_code UK + string permission_name + string module + string permission_type + string action + string description + timestamp created_at + } + + ROLE_PERMISSIONS { + uuid role_permission_id PK + uuid role_id FK + uuid permission_id FK + boolean can_view + boolean can_create + boolean can_edit + boolean can_delete + boolean can_approve + timestamp created_at + } + + USER_ROLES { + uuid user_role_id PK + uuid user_id FK + uuid role_id FK + uuid zone_id FK + uuid region_id FK + uuid area_id FK + timestamp assigned_at + uuid assigned_by FK + timestamp created_at + } + + %% ============================================ + %% ORGANIZATIONAL HIERARCHY + %% ============================================ + ZONES { + uuid zone_id PK + string zone_code UK + string zone_name + string description + boolean is_active + uuid zonal_business_head_id FK + timestamp created_at + timestamp updated_at + } + + REGIONS { + uuid region_id PK + uuid zone_id FK + uuid state_id FK + string region_code UK + string region_name + string description + boolean is_active + timestamp created_at + timestamp updated_at + } + + STATES { + uuid state_id PK + string state_name UK + uuid zone_id FK + boolean is_active + timestamp created_at + } + + DISTRICTS { + uuid district_id PK + uuid state_id FK + string district_name + boolean is_active + timestamp created_at + } + + AREAS { + uuid area_id PK + uuid region_id FK + uuid district_id FK + string area_code UK + string area_name + string city + string pincode + boolean is_active + timestamp created_at + timestamp updated_at + } + + ZONE_MANAGERS { + uuid zone_manager_id PK + uuid zone_id FK + uuid user_id FK + string manager_type + boolean is_active + timestamp assigned_at + timestamp created_at + } + + REGION_MANAGERS { + uuid region_manager_id PK + uuid region_id FK + uuid user_id FK + string manager_type + boolean is_active + timestamp assigned_at + timestamp created_at + } + + AREA_MANAGERS { + uuid area_manager_id PK + uuid area_id FK + uuid user_id FK + string manager_type + boolean is_active + timestamp assigned_at + timestamp created_at + } + + DISTRICT_MANAGERS { + uuid district_manager_id PK + uuid district_id FK + uuid user_id FK + string manager_type + boolean is_active + timestamp assigned_at + timestamp created_at + } + + %% ============================================ + %% OPPORTUNITY MANAGEMENT + %% ============================================ + OPPORTUNITIES { + uuid opportunity_id PK + uuid zone_id FK + uuid region_id FK + uuid state_id FK + uuid district_id FK + string city + string opportunity_type + integer capacity + string priority + date open_from + date open_to + string status + text notes + uuid created_by FK + timestamp created_at + timestamp updated_at + } + + %% ============================================ + %% DEALER ENTITY (POST-ONBOARDING) + %% ============================================ + DEALERS { + uuid dealer_id PK + uuid application_id FK + string dealer_code UK + string dealer_name + string constitution_type + text registered_address + string gst_number + string pan_number + string status + date activation_date + date last_working_day + boolean portal_access_active + timestamp created_at + timestamp updated_at + } + + %% ============================================ + %% DEALER APPLICATION + %% ============================================ + APPLICATIONS { + uuid application_id PK + string registration_number UK + string applicant_name + string email UK + string mobile_number + integer age + string country + string state + string district + string pincode + string interested_city + string company_name + string education_qualification + boolean owns_re_bike + boolean is_existing_dealer + text address + text description + string preferred_location + string application_status + string opportunity_status + boolean is_shortlisted + boolean dd_lead_shortlisted + uuid assigned_to FK + uuid zone_id FK + uuid region_id FK + uuid area_id FK + uuid assigned_dd_zm FK + uuid assigned_rbm FK + timestamp submitted_at + timestamp created_at + timestamp updated_at + } + + %% ============================================ + %% QUESTIONNAIRE MANAGEMENT + %% ============================================ + QUESTIONNAIRES { + uuid questionnaire_id PK + string questionnaire_code UK + string version + string title + text description + boolean is_active + uuid created_by FK + timestamp created_at + timestamp updated_at + } + + QUESTIONNAIRE_SECTIONS { + uuid section_id PK + uuid questionnaire_id FK + string section_name + string section_type + integer section_order + decimal section_weightage + boolean is_active + timestamp created_at + } + + QUESTIONNAIRE_QUESTIONS { + uuid question_id PK + uuid section_id FK + string question_text + string question_type + integer question_order + decimal question_weightage + json options + boolean is_mandatory + boolean is_active + timestamp created_at + timestamp updated_at + } + + QUESTIONNAIRE_RESPONSES { + uuid response_id PK + uuid application_id FK + uuid questionnaire_id FK + uuid question_id FK + text response_text + json response_data + decimal score_obtained + decimal max_score + timestamp submitted_at + timestamp created_at + timestamp updated_at + } + + QUESTIONNAIRE_SCORES { + uuid score_id PK + uuid application_id FK + uuid questionnaire_id FK + decimal total_score + decimal max_score + decimal percentage_score + integer rank_in_city + integer rank_in_region + timestamp calculated_at + timestamp created_at + timestamp updated_at + } + + %% ============================================ + %% INTERVIEW MANAGEMENT + %% ============================================ + INTERVIEWS { + uuid interview_id PK + uuid application_id FK + integer interview_level + string interview_type + string interview_mode + date interview_date + time interview_time + string meeting_link + string venue_address + string status + uuid scheduled_by FK + timestamp scheduled_at + timestamp created_at + timestamp updated_at + } + + INTERVIEW_PARTICIPANTS { + uuid participant_id PK + uuid interview_id FK + uuid user_id FK + string participant_role + boolean is_required + boolean has_attended + timestamp created_at + } + + INTERVIEW_EVALUATIONS { + uuid evaluation_id PK + uuid interview_id FK + uuid application_id FK + uuid evaluator_id FK + integer interview_level + decimal kt_matrix_score + decimal feedback_score + decimal overall_score + string recommendation + text remarks + text feedback_summary + timestamp submitted_at + timestamp created_at + timestamp updated_at + } + + KT_MATRIX_SCORES { + uuid kt_score_id PK + uuid evaluation_id FK + string parameter_name + decimal parameter_weightage + decimal score_obtained + text remarks + timestamp created_at + } + + INTERVIEW_FEEDBACK { + uuid feedback_id PK + uuid evaluation_id FK + string feedback_category + text feedback_text + decimal category_score + timestamp created_at + } + + AI_SUMMARIES { + uuid summary_id PK + uuid application_id FK + integer interview_level + text ai_generated_summary + text nbh_edited_summary + boolean is_approved + uuid approved_by FK + timestamp generated_at + timestamp approved_at + timestamp created_at + timestamp updated_at + } + + %% ============================================ + %% WORK NOTES & COMMUNICATION + %% ============================================ + WORK_NOTES { + uuid work_note_id PK + uuid application_id FK + uuid created_by FK + text note_content + json tagged_users + json attachments + string note_type + uuid parent_note_id FK + boolean is_internal + boolean is_deleted + timestamp created_at + timestamp updated_at + } + + WORK_NOTE_TAGS { + uuid tag_id PK + uuid work_note_id FK + uuid tagged_user_id FK + boolean is_notified + timestamp notified_at + timestamp created_at + } + + WORK_NOTE_ATTACHMENTS { + uuid attachment_id PK + uuid work_note_id FK + uuid document_id FK + timestamp created_at + } + + %% ============================================ + %% DOCUMENT MANAGEMENT + %% ============================================ + DOCUMENTS { + uuid document_id PK + uuid application_id FK + uuid uploaded_by FK + string document_type + string document_category + string file_name + string file_type + bigint file_size + string file_path + string storage_url + string mime_type + integer version + string status + uuid verified_by FK + timestamp verified_at + boolean is_deleted + timestamp uploaded_at + timestamp created_at + timestamp updated_at + } + + DOCUMENT_VERSIONS { + uuid version_id PK + uuid document_id FK + integer version_number + string file_path + string storage_url + uuid uploaded_by FK + timestamp uploaded_at + timestamp created_at + } + + LOI_DOCUMENTS { + uuid loi_doc_id PK + uuid application_id FK + string document_name + string document_type + uuid document_id FK + boolean is_mandatory + boolean is_uploaded + boolean is_verified + timestamp required_at + timestamp uploaded_at + timestamp verified_at + } + + STATUTORY_DOCUMENTS { + uuid statutory_doc_id PK + uuid application_id FK + string document_name + string document_type + uuid document_id FK + boolean is_mandatory + boolean is_uploaded + boolean is_verified + uuid verified_by FK + timestamp verified_at + timestamp created_at + } + + %% ============================================ + %% FDD PROCESS + %% ============================================ + FDD_ASSIGNMENTS { + uuid fdd_assignment_id PK + uuid application_id FK + uuid fdd_user_id FK + string assignment_status + date assigned_at + date due_date + timestamp created_at + timestamp updated_at + } + + FDD_REPORTS { + uuid fdd_report_id PK + uuid fdd_assignment_id FK + uuid application_id FK + string report_type + string report_level + uuid document_id FK + text remarks + string status + timestamp submitted_at + timestamp created_at + timestamp updated_at + } + + %% ============================================ + %% LOI PROCESS + %% ============================================ + LOI_REQUESTS { + uuid loi_request_id PK + uuid application_id FK + string request_status + date document_request_date + date security_deposit_request_date + boolean documents_complete + boolean security_deposit_verified + boolean can_send_back + boolean can_revoke + timestamp created_at + timestamp updated_at + } + + LOI_APPROVALS { + uuid loi_approval_id PK + uuid loi_request_id FK + uuid application_id FK + integer approval_level + uuid approver_id FK + string approval_status + text approval_remarks + timestamp approved_at + timestamp created_at + timestamp updated_at + } + + LOI_DOCUMENTS_GENERATED { + uuid loi_doc_gen_id PK + uuid application_id FK + uuid loi_request_id FK + uuid document_id FK + date issue_date + uuid authorized_signatory FK + string document_version + timestamp generated_at + timestamp uploaded_at + } + + LOI_ACKNOWLEDGEMENTS { + uuid loi_ack_id PK + uuid application_id FK + uuid loi_doc_gen_id FK + uuid document_id FK + timestamp acknowledged_at + timestamp created_at + } + + %% ============================================ + %% SECURITY DEPOSIT + %% ============================================ + SECURITY_DEPOSITS { + uuid deposit_id PK + uuid application_id FK + decimal deposit_amount + string payment_mode + string transaction_id + date transaction_date + string bank_name + string account_number + uuid proof_document_id FK + string verification_status + uuid verified_by FK + timestamp verified_at + text verification_remarks + timestamp created_at + timestamp updated_at + } + + %% ============================================ + %% DEALER CODE GENERATION + %% ============================================ + DEALER_CODES { + uuid dealer_code_id PK + uuid application_id FK + string dealer_code UK + string sales_code + string service_code + string gma_code + string gear_code + string sap_dealer_id + boolean is_active + timestamp generated_at + uuid generated_by FK + timestamp created_at + timestamp updated_at + } + + %% ============================================ + %% DEALER SELF-SERVICE (SECTION 12) + %% ============================================ + DEALER_RESIGNATIONS { + uuid resignation_id PK + uuid dealer_id FK + string outlet_code + date last_operational_date_sales + date last_operational_date_service + date proposed_lwd + string reason_type + text reason_description + string status + boolean is_withdrawn + timestamp submitted_at + timestamp updated_at + } + + DEALER_RELOCATIONS { + uuid relocation_id PK + uuid dealer_id FK + string current_location_json + string proposed_location_json + decimal distance_km + string property_type + date expected_relocation_date + text reason + string status + timestamp submitted_at + } + + DEALER_CONSTITUTION_CHANGES { + uuid constitution_change_id PK + uuid dealer_id FK + string current_constitution + string proposed_constitution + text reason + json new_partners_details + json shareholding_pattern + string status + timestamp submitted_at + } + + %% ============================================ + %% ARCHITECTURAL WORK + %% ============================================ + ARCHITECTURAL_ASSIGNMENTS { + uuid arch_assignment_id PK + uuid application_id FK + uuid assigned_to_team FK + string assignment_status + date assigned_at + date due_date + timestamp created_at + timestamp updated_at + } + + ARCHITECTURAL_DOCUMENTS { + uuid arch_doc_id PK + uuid arch_assignment_id FK + uuid application_id FK + string document_type + uuid document_id FK + string layout_type + boolean dealer_consent_received + date consent_date + timestamp uploaded_at + timestamp created_at + } + + CONSTRUCTION_PROGRESS { + uuid progress_id PK + uuid application_id FK + string progress_type + text progress_description + uuid document_id FK + integer progress_percentage + timestamp reported_at + timestamp created_at + } + + %% ============================================ + %% EOR CHECKLIST + %% ============================================ + EOR_CHECKLISTS { + uuid eor_checklist_id PK + uuid application_id FK + string checklist_name + string status + integer total_items + integer completed_items + integer percentage_complete + timestamp created_at + timestamp updated_at + } + + EOR_CHECKLIST_ITEMS { + uuid eor_item_id PK + uuid eor_checklist_id FK + string item_name + string item_category + string responsible_team + string status + text remarks + uuid verified_by FK + timestamp verified_at + timestamp created_at + timestamp updated_at + } + + %% ============================================ + %% LOA PROCESS + %% ============================================ + LOA_REQUESTS { + uuid loa_request_id PK + uuid application_id FK + string request_status + boolean eor_complete + boolean loa_issued_before_eor + timestamp created_at + timestamp updated_at + } + + LOA_APPROVALS { + uuid loa_approval_id PK + uuid loa_request_id FK + uuid application_id FK + uuid approver_id FK + string approval_status + text approval_remarks + timestamp approved_at + timestamp created_at + timestamp updated_at + } + + LOA_DOCUMENTS_GENERATED { + uuid loa_doc_gen_id PK + uuid application_id FK + uuid loa_request_id FK + uuid document_id FK + date issue_date + uuid authorized_signatory FK + string document_version + timestamp generated_at + timestamp uploaded_at + } + + %% ============================================ + %% INAUGURATION + %% ============================================ + INAUGURATIONS { + uuid inauguration_id PK + uuid application_id FK + date inauguration_date + string venue + text event_summary + uuid organized_by FK + string status + timestamp created_at + timestamp updated_at + } + + INAUGURATION_ATTENDEES { + uuid attendee_id PK + uuid inauguration_id FK + uuid user_id FK + string attendee_role + boolean is_confirmed + timestamp created_at + } + + INAUGURATION_DOCUMENTS { + uuid inauguration_doc_id PK + uuid inauguration_id FK + uuid document_id FK + string document_type + timestamp uploaded_at + } + + %% ============================================ + %% TERMINATION & F&F SETTLEMENT (SECTION 4.3 & 10) + %% ============================================ + TERMINATION_REQUESTS { + uuid termination_id PK + uuid dealer_id FK + string category + text reason + date proposed_lwd + string status + uuid initiated_by FK + timestamp created_at + } + + TERMINATION_APPROVALS { + uuid approval_id PK + uuid termination_id FK + integer level + string approver_role + uuid approver_id FK + string action + text remarks + timestamp created_at + } + + FNF_CASES { + uuid fnf_id PK + uuid dealer_id FK + uuid source_id FK + string source_type + date last_working_day + string status + timestamp initiated_at + } + + FNF_DEPARTMENT_CLEARANCES { + uuid clearance_id PK + uuid fnf_id FK + string department_name + string clearance_status + decimal payable_amount + decimal recovery_amount + text remarks + uuid cleared_by FK + timestamp cleared_at + } + + FNF_LINE_ITEMS { + uuid line_item_id PK + uuid fnf_id FK + uuid clearance_id FK + string item_type + string department + string description + decimal amount + uuid added_by FK + timestamp created_at + } + + FNF_SETTLEMENT_SUMMARIES { + uuid summary_id PK + uuid fnf_id FK + decimal total_payables + decimal total_receivables + decimal total_deductions + decimal net_settlement_amount + string status + uuid verified_by FK + timestamp verified_at + } + + %% ============================================ + %% NOTIFICATIONS + %% ============================================ + NOTIFICATIONS { + uuid notification_id PK + uuid user_id FK + uuid application_id FK + string notification_type + string title + text message + string priority + boolean is_read + json metadata + timestamp read_at + timestamp created_at + } + + EMAIL_LOGS { + uuid email_log_id PK + uuid application_id FK + uuid user_id FK + string email_type + string recipient_email + string subject + text email_body + string status + text error_message + timestamp sent_at + timestamp created_at + } + + WHATSAPP_LOGS { + uuid whatsapp_log_id PK + uuid application_id FK + string recipient_number + string message_type + text message_content + string status + text error_message + timestamp sent_at + timestamp created_at + } + + %% ============================================ + %% SLA & TAT TRACKING + %% ============================================ + SLA_CONFIGURATIONS { + uuid sla_config_id PK + string activity_name + string owner_role + integer tat_hours + string tat_unit + boolean is_active + timestamp created_at + timestamp updated_at + } + + SLA_CONFIG_REMINDERS { + uuid reminder_id PK + uuid sla_config_id FK + integer time_value + string time_unit + boolean is_enabled + timestamp created_at + } + + SLA_CONFIG_ESCALATIONS { + uuid escalation_config_id PK + uuid sla_config_id FK + integer level + integer time_value + string time_unit + string notify_email + timestamp created_at + } + + SLA_TRACKING { + uuid sla_tracking_id PK + uuid application_id FK + uuid sla_config_id FK + string activity_name + uuid owner_id FK + timestamp start_time + timestamp due_time + timestamp completed_time + string status + integer completion_percentage + timestamp created_at + timestamp updated_at + } + + SLA_ESCALATIONS { + uuid escalation_id PK + uuid sla_tracking_id FK + integer escalation_level + uuid escalated_to FK + text escalation_reason + timestamp escalated_at + timestamp resolved_at + } + + %% ============================================ + %% EMAIL TEMPLATES + %% ============================================ + EMAIL_TEMPLATES { + uuid template_id PK + string template_name UK + string template_code UK + string subject + text template_body + string trigger_event + json system_variables + boolean is_active + string version + timestamp created_at + timestamp updated_at + } + + %% ============================================ + %% AUDIT TRAIL + %% ============================================ + AUDIT_LOGS { + uuid audit_log_id PK + uuid application_id FK + uuid user_id FK + string action_type + string entity_type + uuid entity_id + text description + json before_state + json after_state + json metadata + string ip_address + string user_agent + timestamp created_at + } + + ACTIVITY_LOGS { + uuid activity_id PK + uuid application_id FK + uuid user_id FK + string activity_type + text activity_description + json activity_data + timestamp created_at + } + + %% ============================================ + %% APPLICATION STATUS TRACKING + %% ============================================ + APPLICATION_STATUS_HISTORY { + uuid status_history_id PK + uuid application_id FK + string previous_status + string new_status + uuid changed_by FK + text change_reason + timestamp changed_at + } + + APPLICATION_PROGRESS { + uuid progress_id PK + uuid application_id FK + string stage_name + integer stage_order + string status + integer completion_percentage + timestamp stage_started_at + timestamp stage_completed_at + timestamp created_at + timestamp updated_at + } + + WORKFLOW_STAGES_CONFIG { + uuid stage_config_id PK + string stage_name UK + integer stage_order + string color_code + boolean is_parallel + json default_evaluators + boolean is_active + timestamp created_at + } + + %% ============================================ + %% DEALER PORTAL ACCESS CONFIG + %% ============================================ + DEALER_PORTAL_CONFIG { + uuid config_id PK + uuid dealer_id FK + boolean allows_resignation + boolean allows_relocation + boolean allows_constitution_change + timestamp access_revoked_at + } + + %% ============================================ + %% RELATIONSHIPS + %% ============================================ + USERS ||--o{ USER_ROLES : "has" + ROLES ||--o{ USER_ROLES : "assigned_to" + ROLES ||--o{ ROLE_PERMISSIONS : "has" + PERMISSIONS ||--o{ ROLE_PERMISSIONS : "granted_in" + + ZONES ||--o{ STATES : "contains" + STATES ||--o{ DISTRICTS : "contains" + ZONES ||--o{ REGIONS : "contains" + STATES ||--o{ REGIONS : "contains" + REGIONS ||--o{ AREAS : "contains" + DISTRICTS ||--o{ AREAS : "contains" + + ZONES ||--o{ ZONE_MANAGERS : "managed_by" + REGIONS ||--o{ REGION_MANAGERS : "managed_by" + AREAS ||--o{ AREA_MANAGERS : "managed_by" + USERS ||--o{ ZONE_MANAGERS : "is" + USERS ||--o{ REGION_MANAGERS : "is" + USERS ||--o{ AREA_MANAGERS : "is" + + DISTRICTS ||--o{ DISTRICT_MANAGERS : "managed_by" + USERS ||--o{ DISTRICT_MANAGERS : "is" + + ZONES ||--o{ OPPORTUNITIES : "has" + REGIONS ||--o{ OPPORTUNITIES : "has" + STATES ||--o{ OPPORTUNITIES : "has" + DISTRICTS ||--o{ OPPORTUNITIES : "has" + + ZONES ||--o{ APPLICATIONS : "belongs_to" + REGIONS ||--o{ APPLICATIONS : "belongs_to" + AREAS ||--o{ APPLICATIONS : "belongs_to" + USERS ||--o{ APPLICATIONS : "assigned_to" + + APPLICATIONS ||--o{ QUESTIONNAIRE_RESPONSES : "has" + QUESTIONNAIRES ||--o{ QUESTIONNAIRE_RESPONSES : "used_in" + QUESTIONNAIRE_QUESTIONS ||--o{ QUESTIONNAIRE_RESPONSES : "answered_by" + APPLICATIONS ||--o{ QUESTIONNAIRE_SCORES : "has" + + APPLICATIONS ||--o{ INTERVIEWS : "has" + INTERVIEWS ||--o{ INTERVIEW_PARTICIPANTS : "includes" + INTERVIEWS ||--o{ INTERVIEW_EVALUATIONS : "evaluated_in" + INTERVIEW_EVALUATIONS ||--o{ KT_MATRIX_SCORES : "contains" + INTERVIEW_EVALUATIONS ||--o{ INTERVIEW_FEEDBACK : "has" + APPLICATIONS ||--o{ AI_SUMMARIES : "has" + + APPLICATIONS ||--o{ WORK_NOTES : "has" + WORK_NOTES ||--o{ WORK_NOTE_TAGS : "tags" + WORK_NOTES ||--o{ WORK_NOTE_ATTACHMENTS : "has" + DOCUMENTS ||--o{ WORK_NOTE_ATTACHMENTS : "attached_to" + + APPLICATIONS ||--o{ DOCUMENTS : "has" + DOCUMENTS ||--o{ DOCUMENT_VERSIONS : "has" + APPLICATIONS ||--o{ LOI_DOCUMENTS : "requires" + APPLICATIONS ||--o{ STATUTORY_DOCUMENTS : "requires" + + APPLICATIONS ||--o{ FDD_ASSIGNMENTS : "assigned_to" + FDD_ASSIGNMENTS ||--o{ FDD_REPORTS : "generates" + FDD_REPORTS ||--o{ DOCUMENTS : "stored_as" + + APPLICATIONS ||--o{ LOI_REQUESTS : "has" + LOI_REQUESTS ||--o{ LOI_APPROVALS : "requires" + LOI_REQUESTS ||--o{ LOI_DOCUMENTS_GENERATED : "generates" + LOI_DOCUMENTS_GENERATED ||--o{ LOI_ACKNOWLEDGEMENTS : "acknowledged_by" + + APPLICATIONS ||--o{ SECURITY_DEPOSITS : "has" + SECURITY_DEPOSITS ||--o{ DOCUMENTS : "proof_document" + + APPLICATIONS ||--o{ DEALER_CODES : "has" + APPLICATIONS ||--o{ DEALERS : "onboarded_as" + DEALERS ||--o{ USERS : "has_portal_users" + + DEALERS ||--o{ DEALER_RESIGNATIONS : "initiates" + DEALERS ||--o{ DEALER_RELOCATIONS : "requests" + DEALERS ||--o{ DEALER_CONSTITUTION_CHANGES : "proposes" + + DEALERS ||--o{ TERMINATION_REQUESTS : "terminated_by" + TERMINATION_REQUESTS ||--o{ TERMINATION_APPROVALS : "requires" + + DEALERS ||--o{ FNF_CASES : "settled_in" + FNF_CASES ||--o{ FNF_DEPARTMENT_CLEARANCES : "requires_NOC_from" + FNF_CASES ||--o{ FNF_SETTLEMENT_SUMMARIES : "consolidated_in" + + DEALERS ||--o{ DEALER_PORTAL_CONFIG : "governed_by" + + APPLICATIONS ||--o{ ARCHITECTURAL_ASSIGNMENTS : "assigned_to" + ARCHITECTURAL_ASSIGNMENTS ||--o{ ARCHITECTURAL_DOCUMENTS : "has" + APPLICATIONS ||--o{ CONSTRUCTION_PROGRESS : "tracks" + + APPLICATIONS ||--o{ EOR_CHECKLISTS : "has" + EOR_CHECKLISTS ||--o{ EOR_CHECKLIST_ITEMS : "contains" + + APPLICATIONS ||--o{ LOA_REQUESTS : "has" + LOA_REQUESTS ||--o{ LOA_APPROVALS : "requires" + LOA_REQUESTS ||--o{ LOA_DOCUMENTS_GENERATED : "generates" + + APPLICATIONS ||--o{ INAUGURATIONS : "has" + INAUGURATIONS ||--o{ INAUGURATION_ATTENDEES : "includes" + INAUGURATIONS ||--o{ INAUGURATION_DOCUMENTS : "has" + + USERS ||--o{ NOTIFICATIONS : "receives" + APPLICATIONS ||--o{ NOTIFICATIONS : "triggers" + APPLICATIONS ||--o{ EMAIL_LOGS : "triggers" + APPLICATIONS ||--o{ WHATSAPP_LOGS : "triggers" + + SLA_CONFIGURATIONS ||--o{ SLA_TRACKING : "tracks" + APPLICATIONS ||--o{ SLA_TRACKING : "monitored_by" + SLA_TRACKING ||--o{ SLA_ESCALATIONS : "escalates" + + APPLICATIONS ||--o{ AUDIT_LOGS : "logged_in" + USERS ||--o{ AUDIT_LOGS : "performed_by" + APPLICATIONS ||--o{ ACTIVITY_LOGS : "logged_in" + APPLICATIONS ||--o{ APPLICATION_STATUS_HISTORY : "tracks" + APPLICATIONS ||--o{ APPLICATION_PROGRESS : "tracks" + + SLA_CONFIGURATIONS ||--o{ SLA_CONFIG_REMINDERS : "defines" + SLA_CONFIGURATIONS ||--o{ SLA_CONFIG_ESCALATIONS : "defines" + FNF_CASES ||--o{ FNF_LINE_ITEMS : "has" + FNF_DEPARTMENT_CLEARANCES ||--o{ FNF_LINE_ITEMS : "details" + USERS ||--o{ FNF_LINE_ITEMS : "added" + USERS ||--o{ APPLICATIONS : "currently_assigned" + + diff --git a/middleware/auth.js b/middleware/auth.js new file mode 100644 index 0000000..c68c23c --- /dev/null +++ b/middleware/auth.js @@ -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 +}; diff --git a/middleware/errorHandler.js b/middleware/errorHandler.js new file mode 100644 index 0000000..6f54581 --- /dev/null +++ b/middleware/errorHandler.js @@ -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; diff --git a/middleware/roleCheck.js b/middleware/roleCheck.js new file mode 100644 index 0000000..af26545 --- /dev/null +++ b/middleware/roleCheck.js @@ -0,0 +1,47 @@ +const { ROLES } = require('../config/constants'); +const logger = require('../utils/logger'); + +/** + * Role-based access control middleware + * @param {Array} 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 +}; diff --git a/middleware/upload.js b/middleware/upload.js new file mode 100644 index 0000000..1d2237d --- /dev/null +++ b/middleware/upload.js @@ -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 +}; diff --git a/models/Application.js b/models/Application.js new file mode 100644 index 0000000..64ad663 --- /dev/null +++ b/models/Application.js @@ -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; +}; diff --git a/models/AuditLog.js b/models/AuditLog.js new file mode 100644 index 0000000..1245e08 --- /dev/null +++ b/models/AuditLog.js @@ -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; +}; diff --git a/models/ConstitutionalChange.js b/models/ConstitutionalChange.js new file mode 100644 index 0000000..88a5538 --- /dev/null +++ b/models/ConstitutionalChange.js @@ -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; +}; diff --git a/models/Document.js b/models/Document.js new file mode 100644 index 0000000..31f7683 --- /dev/null +++ b/models/Document.js @@ -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; +}; diff --git a/models/FinancePayment.js b/models/FinancePayment.js new file mode 100644 index 0000000..2ab1ec6 --- /dev/null +++ b/models/FinancePayment.js @@ -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; +}; diff --git a/models/FnF.js b/models/FnF.js new file mode 100644 index 0000000..03bf4d9 --- /dev/null +++ b/models/FnF.js @@ -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; +}; diff --git a/models/Outlet.js b/models/Outlet.js new file mode 100644 index 0000000..6691a94 --- /dev/null +++ b/models/Outlet.js @@ -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; +}; diff --git a/models/Region.js b/models/Region.js new file mode 100644 index 0000000..7d6a644 --- /dev/null +++ b/models/Region.js @@ -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; +}; diff --git a/models/RelocationRequest.js b/models/RelocationRequest.js new file mode 100644 index 0000000..903b687 --- /dev/null +++ b/models/RelocationRequest.js @@ -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; +}; diff --git a/models/Resignation.js b/models/Resignation.js new file mode 100644 index 0000000..b1af1b5 --- /dev/null +++ b/models/Resignation.js @@ -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; +}; diff --git a/models/User.js b/models/User.js new file mode 100644 index 0000000..769572e --- /dev/null +++ b/models/User.js @@ -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; +}; diff --git a/models/Worknote.js b/models/Worknote.js new file mode 100644 index 0000000..67c6711 --- /dev/null +++ b/models/Worknote.js @@ -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; +}; diff --git a/models/Zone.js b/models/Zone.js new file mode 100644 index 0000000..a780694 --- /dev/null +++ b/models/Zone.js @@ -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; +}; diff --git a/models/index.js b/models/index.js new file mode 100644 index 0000000..946d34f --- /dev/null +++ b/models/index.js @@ -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; diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..ef58e5a --- /dev/null +++ b/package-lock.json @@ -0,0 +1,6039 @@ +{ + "name": "royal-enfield-onboarding-backend", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "royal-enfield-onboarding-backend", + "version": "1.0.0", + "license": "PROPRIETARY", + "dependencies": { + "bcryptjs": "^2.4.3", + "compression": "^1.7.4", + "cors": "^2.8.5", + "dotenv": "^16.3.1", + "express": "^4.18.2", + "express-rate-limit": "^7.1.5", + "express-validator": "^7.0.1", + "helmet": "^7.1.0", + "jsonwebtoken": "^9.0.2", + "multer": "^1.4.5-lts.1", + "nodemailer": "^6.9.7", + "pg": "^8.11.3", + "pg-hstore": "^2.3.4", + "sequelize": "^6.35.2", + "uuid": "^9.0.1", + "winston": "^3.11.0" + }, + "devDependencies": { + "jest": "^29.7.0", + "nodemon": "^3.0.2", + "supertest": "^6.3.3" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=9.0.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.28.6.tgz", + "integrity": "sha512-JYgintcMjRiCvS8mMECzaEn+m3PfoQiyqukOMCCVQtoJGYJw8j/8LBJEiqkHLkfwCcs74E3pbAUFNg7d9VNJ+Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.28.5", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.6.tgz", + "integrity": "sha512-2lfu57JtzctfIrcGMz992hyLlByuzgIk58+hhGCxjKZ3rWI82NnVLjXcaTqkI2NvlcvOskZaiZ5kjUALo3Lpxg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.6.tgz", + "integrity": "sha512-H3mcG6ZDLTlYfaSNi0iOKkigqMFvkTKlGUYlD8GW7nNOYRrevuA46iTypPyv+06V3fEmvvazfntkBU34L0azAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.28.6", + "@babel/generator": "^7.28.6", + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-module-transforms": "^7.28.6", + "@babel/helpers": "^7.28.6", + "@babel/parser": "^7.28.6", + "@babel/template": "^7.28.6", + "@babel/traverse": "^7.28.6", + "@babel/types": "^7.28.6", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/core/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@babel/core/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@babel/generator": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.6.tgz", + "integrity": "sha512-lOoVRwADj8hjf7al89tvQ2a1lf53Z+7tiXMgpZJL3maQPDxh0DgLMN62B2MKUOFcoodBHLMbDM6WAbKgNy5Suw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.28.6", + "@babel/types": "^7.28.6", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz", + "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.28.6", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", + "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz", + "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.28.6", + "@babel/helper-validator-identifier": "^7.28.5", + "@babel/traverse": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz", + "integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.6.tgz", + "integrity": "sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.6.tgz", + "integrity": "sha512-TeR9zWR18BvbfPmGbLampPMW+uW1NZnJlRuuHso8i87QZNq2JRF9i6RgxRqtEq+wQGsS19NNTWr2duhnE49mfQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.6" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-syntax-async-generators": { + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", + "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-bigint": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz", + "integrity": "sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-properties": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", + "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.12.13" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-static-block": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz", + "integrity": "sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-attributes": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.28.6.tgz", + "integrity": "sha512-jiLC0ma9XkQT3TKJ9uYvlakm66Pamywo+qwL+oL8HJOvc6TWdZXVfhqJr8CCzbSGUAbDOzlGHJC1U+vRfLQDvw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-meta": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", + "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-json-strings": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", + "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-jsx": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.28.6.tgz", + "integrity": "sha512-wgEmr06G6sIpqr8YDwA2dSRTE3bJ+V0IfpzfSY3Lfgd7YWOaAdlykvJi13ZKBt8cZHfgH1IXN+CL656W3uUa4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-logical-assignment-operators": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", + "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-nullish-coalescing-operator": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", + "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-numeric-separator": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", + "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-object-rest-spread": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", + "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-catch-binding": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", + "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-chaining": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", + "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-private-property-in-object": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz", + "integrity": "sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-top-level-await": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", + "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-typescript": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.28.6.tgz", + "integrity": "sha512-+nDNmQye7nlnuuHDboPbGm00Vqg3oO8niRRL27/4LYHUsHYh0zJ1xWOz0uRwNFmM1Avzk8wZbc6rdiYhomzv/A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", + "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.28.6", + "@babel/parser": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.6.tgz", + "integrity": "sha512-fgWX62k02qtjqdSNTAGxmKYY/7FSL9WAS1o2Hu5+I5m9T0yxZzr4cnrfXQ/MX0rIifthCSs6FKTlzYbJcPtMNg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.28.6", + "@babel/generator": "^7.28.6", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.28.6", + "@babel/template": "^7.28.6", + "@babel/types": "^7.28.6", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@babel/traverse/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@babel/types": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.6.tgz", + "integrity": "sha512-0ZrskXVEHSWIqZM/sQZ4EV3jZJXRkio/WCxaqKZP1g//CEWEPSfeZFcms4XeKBCHU0ZKnIkdJeU/kF+eRp5lBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@bcoe/v8-coverage": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", + "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@colors/colors": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.6.0.tgz", + "integrity": "sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA==", + "license": "MIT", + "engines": { + "node": ">=0.1.90" + } + }, + "node_modules/@dabh/diagnostics": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/@dabh/diagnostics/-/diagnostics-2.0.8.tgz", + "integrity": "sha512-R4MSXTVnuMzGD7bzHdW2ZhhdPC/igELENcq5IjEverBvq5hn1SXCWcsi6eSsdWP0/Ur+SItRRjAktmdoX/8R/Q==", + "license": "MIT", + "dependencies": { + "@so-ric/colorspace": "^1.1.6", + "enabled": "2.0.x", + "kuler": "^2.0.0" + } + }, + "node_modules/@istanbuljs/load-nyc-config": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", + "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "camelcase": "^5.3.1", + "find-up": "^4.1.0", + "get-package-type": "^0.1.0", + "js-yaml": "^3.13.1", + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/schema": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", + "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/console": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/console/-/console-29.7.0.tgz", + "integrity": "sha512-5Ni4CU7XHQi32IJ398EEP4RrB8eV09sXP2ROqD4bksHrnTree52PsxvX8tpL8LvTZ3pFzXyPbNQReSN41CAhOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/core": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/core/-/core-29.7.0.tgz", + "integrity": "sha512-n7aeXWKMnGtDA48y8TLWJPJmLmmZ642Ceo78cYWEpiD7FzDgmNDV/GCVRorPABdXLJZ/9wzzgZAlHjXjxDHGsg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/reporters": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "ansi-escapes": "^4.2.1", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.9", + "jest-changed-files": "^29.7.0", + "jest-config": "^29.7.0", + "jest-haste-map": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-resolve-dependencies": "^29.7.0", + "jest-runner": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "jest-watcher": "^29.7.0", + "micromatch": "^4.0.4", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/@jest/environment": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-29.7.0.tgz", + "integrity": "sha512-aQIfHDq33ExsN4jP1NWGXhxgQ/wixs60gDiKO+XVMd8Mn0NWPWgc34ZQDTb2jKaUWQ7MuwoitXAsN2XVXNMpAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/fake-timers": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-mock": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/expect": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-29.7.0.tgz", + "integrity": "sha512-8uMeAMycttpva3P1lBHB8VciS9V0XAr3GymPpipdyQXbBcuhkLQOSe8E/p92RyAdToS6ZD1tFkX+CkhoECE0dQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "expect": "^29.7.0", + "jest-snapshot": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/expect-utils": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-29.7.0.tgz", + "integrity": "sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "jest-get-type": "^29.6.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/fake-timers": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-29.7.0.tgz", + "integrity": "sha512-q4DH1Ha4TTFPdxLsqDXK1d3+ioSL7yL5oCMJZgDYm6i+6CygW5E5xVr/D1HdsGxjt1ZWSfUAs9OxSB/BNelWrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@sinonjs/fake-timers": "^10.0.2", + "@types/node": "*", + "jest-message-util": "^29.7.0", + "jest-mock": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/globals": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-29.7.0.tgz", + "integrity": "sha512-mpiz3dutLbkW2MNFubUGUEVLkTGiqW6yLVTA+JbP6fI6J5iL9Y0Nlg8k95pcF8ctKwCS7WVxteBs29hhfAotzQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/expect": "^29.7.0", + "@jest/types": "^29.6.3", + "jest-mock": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/reporters": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-29.7.0.tgz", + "integrity": "sha512-DApq0KJbJOEzAFYjHADNNxAE3KbhxQB1y5Kplb5Waqw6zVbuWatSnMjE5gs8FUgEPmNsnZA3NCWl9NG0ia04Pg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@bcoe/v8-coverage": "^0.2.3", + "@jest/console": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@jridgewell/trace-mapping": "^0.3.18", + "@types/node": "*", + "chalk": "^4.0.0", + "collect-v8-coverage": "^1.0.0", + "exit": "^0.1.2", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "istanbul-lib-coverage": "^3.0.0", + "istanbul-lib-instrument": "^6.0.0", + "istanbul-lib-report": "^3.0.0", + "istanbul-lib-source-maps": "^4.0.0", + "istanbul-reports": "^3.1.3", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "jest-worker": "^29.7.0", + "slash": "^3.0.0", + "string-length": "^4.0.1", + "strip-ansi": "^6.0.0", + "v8-to-istanbul": "^9.0.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/@jest/schemas": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", + "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sinclair/typebox": "^0.27.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/source-map": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-29.6.3.tgz", + "integrity": "sha512-MHjT95QuipcPrpLM+8JMSzFx6eHp5Bm+4XeFDJlwsvVBjmKNiIAvasGK2fxz2WbGRlnvqehFbh07MMa7n3YJnw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.18", + "callsites": "^3.0.0", + "graceful-fs": "^4.2.9" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/test-result": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-29.7.0.tgz", + "integrity": "sha512-Fdx+tv6x1zlkJPcWXmMDAG2HBnaR9XPSd5aDWQVsfrZmLVT3lU1cwyxLgRmXR9yrq4NBoEm9BMsfgFzTQAbJYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "collect-v8-coverage": "^1.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/test-sequencer": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-29.7.0.tgz", + "integrity": "sha512-GQwJ5WZVrKnOJuiYiAF52UNUJXgTZx1NHjFSEB0qEMmSZKAkdMoIzw/Cj6x6NF4AvV23AUqDpFzQkN/eYCYTxw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/test-result": "^29.7.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/transform": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-29.7.0.tgz", + "integrity": "sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.11.6", + "@jest/types": "^29.6.3", + "@jridgewell/trace-mapping": "^0.3.18", + "babel-plugin-istanbul": "^6.1.1", + "chalk": "^4.0.0", + "convert-source-map": "^2.0.0", + "fast-json-stable-stringify": "^2.1.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-util": "^29.7.0", + "micromatch": "^4.0.4", + "pirates": "^4.0.4", + "slash": "^3.0.0", + "write-file-atomic": "^4.0.2" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/types": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", + "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^17.0.8", + "chalk": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@noble/hashes": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", + "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@paralleldrive/cuid2": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/@paralleldrive/cuid2/-/cuid2-2.3.1.tgz", + "integrity": "sha512-XO7cAxhnTZl0Yggq6jOgjiOHhbgcO4NqFqwSmQpjK3b6TEE6Uj/jfSk6wzYyemh3+I0sHirKSetjQwn5cZktFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@noble/hashes": "^1.1.5" + } + }, + "node_modules/@sinclair/typebox": { + "version": "0.27.8", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", + "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@sinonjs/commons": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", + "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "type-detect": "4.0.8" + } + }, + "node_modules/@sinonjs/fake-timers": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-10.3.0.tgz", + "integrity": "sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@sinonjs/commons": "^3.0.0" + } + }, + "node_modules/@so-ric/colorspace": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/@so-ric/colorspace/-/colorspace-1.1.6.tgz", + "integrity": "sha512-/KiKkpHNOBgkFJwu9sh48LkHSMYGyuTcSFK/qMBdnOAlrRJzRSXAOFB5qwzaVQuDl8wAvHVMkaASQDReTahxuw==", + "license": "MIT", + "dependencies": { + "color": "^5.0.2", + "text-hex": "1.0.x" + } + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/debug": { + "version": "4.1.12", + "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz", + "integrity": "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==", + "license": "MIT", + "dependencies": { + "@types/ms": "*" + } + }, + "node_modules/@types/graceful-fs": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.9.tgz", + "integrity": "sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/istanbul-lib-coverage": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", + "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/istanbul-lib-report": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.3.tgz", + "integrity": "sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/istanbul-lib-coverage": "*" + } + }, + "node_modules/@types/istanbul-reports": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.4.tgz", + "integrity": "sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/istanbul-lib-report": "*" + } + }, + "node_modules/@types/ms": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", + "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "25.0.9", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.0.9.tgz", + "integrity": "sha512-/rpCXHlCWeqClNBwUhDcusJxXYDjZTyE8v5oTO7WbL8eij2nKhUeU89/6xgjU7N4/Vh3He0BtyhJdQbDyhiXAw==", + "license": "MIT", + "dependencies": { + "undici-types": "~7.16.0" + } + }, + "node_modules/@types/stack-utils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", + "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/triple-beam": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/triple-beam/-/triple-beam-1.3.5.tgz", + "integrity": "sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw==", + "license": "MIT" + }, + "node_modules/@types/validator": { + "version": "13.15.10", + "resolved": "https://registry.npmjs.org/@types/validator/-/validator-13.15.10.tgz", + "integrity": "sha512-T8L6i7wCuyoK8A/ZeLYt1+q0ty3Zb9+qbSSvrIVitzT3YjZqkTZ40IbRsPanlB4h1QB3JVL1SYCdR6ngtFYcuA==", + "license": "MIT" + }, + "node_modules/@types/yargs": { + "version": "17.0.35", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.35.tgz", + "integrity": "sha512-qUHkeCyQFxMXg79wQfTtfndEC+N9ZZg76HJftDJp+qH2tV7Gj4OJi7l+PiWwJ+pWtW8GwSmqsDj/oymhrTWXjg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/@types/yargs-parser": { + "version": "21.0.3", + "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz", + "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/accepts/node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/ansi-escapes": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", + "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^0.21.3" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/append-field": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/append-field/-/append-field-1.0.0.tgz", + "integrity": "sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==", + "license": "MIT" + }, + "node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "license": "MIT", + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", + "license": "MIT" + }, + "node_modules/asap": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", + "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==", + "dev": true, + "license": "MIT" + }, + "node_modules/async": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", + "license": "MIT" + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/babel-jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.7.0.tgz", + "integrity": "sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/transform": "^29.7.0", + "@types/babel__core": "^7.1.14", + "babel-plugin-istanbul": "^6.1.1", + "babel-preset-jest": "^29.6.3", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.8.0" + } + }, + "node_modules/babel-plugin-istanbul": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz", + "integrity": "sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@babel/helper-plugin-utils": "^7.0.0", + "@istanbuljs/load-nyc-config": "^1.0.0", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-instrument": "^5.0.4", + "test-exclude": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/babel-plugin-istanbul/node_modules/istanbul-lib-instrument": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.1.tgz", + "integrity": "sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@babel/core": "^7.12.3", + "@babel/parser": "^7.14.7", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^6.3.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/babel-plugin-jest-hoist": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-29.6.3.tgz", + "integrity": "sha512-ESAc/RJvGTFEzRwOTT4+lNDk/GNHMkKbNzsvT0qKRfDyyYTskxB5rnU2njIDYVxXCBHHEI1c0YwHob3WaYujOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.3.3", + "@babel/types": "^7.3.3", + "@types/babel__core": "^7.1.14", + "@types/babel__traverse": "^7.0.6" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/babel-preset-current-node-syntax": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.2.0.tgz", + "integrity": "sha512-E/VlAEzRrsLEb2+dv8yp3bo4scof3l9nR4lrld+Iy5NyVqgVYUJnDAmunkhPMisRI32Qc4iRiz425d8vM++2fg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/plugin-syntax-async-generators": "^7.8.4", + "@babel/plugin-syntax-bigint": "^7.8.3", + "@babel/plugin-syntax-class-properties": "^7.12.13", + "@babel/plugin-syntax-class-static-block": "^7.14.5", + "@babel/plugin-syntax-import-attributes": "^7.24.7", + "@babel/plugin-syntax-import-meta": "^7.10.4", + "@babel/plugin-syntax-json-strings": "^7.8.3", + "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", + "@babel/plugin-syntax-numeric-separator": "^7.10.4", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", + "@babel/plugin-syntax-optional-chaining": "^7.8.3", + "@babel/plugin-syntax-private-property-in-object": "^7.14.5", + "@babel/plugin-syntax-top-level-await": "^7.14.5" + }, + "peerDependencies": { + "@babel/core": "^7.0.0 || ^8.0.0-0" + } + }, + "node_modules/babel-preset-jest": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-29.6.3.tgz", + "integrity": "sha512-0B3bhxR6snWXJZtR/RliHTDPRgn1sNHOR0yVtq/IiQFyuOVjFS+wuio/R4gSNkyYmKmJB4wGZv2NZanmKmTnNA==", + "dev": true, + "license": "MIT", + "dependencies": { + "babel-plugin-jest-hoist": "^29.6.3", + "babel-preset-current-node-syntax": "^1.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/baseline-browser-mapping": { + "version": "2.9.15", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.15.tgz", + "integrity": "sha512-kX8h7K2srmDyYnXRIppo4AH/wYgzWVCs+eKr3RusRSQ5PvRYoEFmR/I0PbdTjKFAoKqp5+kbxnNTFO9jOfSVJg==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.js" + } + }, + "node_modules/bcryptjs": { + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-2.4.3.tgz", + "integrity": "sha512-V/Hy/X9Vt7f3BbPJEi8BdVFMByHi+jNXrYkW3huaybV/kQ0KJg0Y6PkEMbn+zeT+i+SiKZ/HMqJGIIt4LZDqNQ==", + "license": "MIT" + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/body-parser": { + "version": "1.20.4", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.4.tgz", + "integrity": "sha512-ZTgYYLMOXY9qKU/57FAo8F+HA2dGX7bqGc71txDRC1rS4frdFI5R7NhluHxH6M0YItAP0sHB4uqAOcYKxO6uGA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "~1.2.0", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "on-finished": "~2.4.1", + "qs": "~6.14.0", + "raw-body": "~2.5.3", + "type-is": "~1.6.18", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", + "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.9.0", + "caniuse-lite": "^1.0.30001759", + "electron-to-chromium": "^1.5.263", + "node-releases": "^2.0.27", + "update-browserslist-db": "^1.2.0" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/bser": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz", + "integrity": "sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "node-int64": "^0.4.0" + } + }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", + "license": "BSD-3-Clause" + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "license": "MIT" + }, + "node_modules/busboy": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", + "integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==", + "dependencies": { + "streamsearch": "^1.1.0" + }, + "engines": { + "node": ">=10.16.0" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001765", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001765.tgz", + "integrity": "sha512-LWcNtSyZrakjECqmpP4qdg0MMGdN368D7X8XvvAqOcqMv0RxnlqVKZl2V6/mBR68oYMxOZPLw/gO7DuisMHUvQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/char-regex": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz", + "integrity": "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/ci-info": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", + "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/cjs-module-lexer": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.4.3.tgz", + "integrity": "sha512-9z8TZaGM1pfswYeXrUpzPrkx8UnWYdhJclsiYMm6x/w5+nN+8Tf/LnAgfLGQCm59qAOxU8WwHEq2vNwF6i4j+Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/co": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", + "integrity": "sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">= 1.0.0", + "node": ">= 0.12.0" + } + }, + "node_modules/collect-v8-coverage": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.3.tgz", + "integrity": "sha512-1L5aqIkwPfiodaMgQunkF1zRhNqifHBmtbbbxcr6yVxxBnliw4TDOW6NxpO8DJLgJ16OT+Y4ztZqP6p/FtXnAw==", + "dev": true, + "license": "MIT" + }, + "node_modules/color": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/color/-/color-5.0.3.tgz", + "integrity": "sha512-ezmVcLR3xAVp8kYOm4GS45ZLLgIE6SPAFoduLr6hTDajwb3KZ2F46gulK3XpcwRFb5KKGCSezCBAY4Dw4HsyXA==", + "license": "MIT", + "dependencies": { + "color-convert": "^3.1.3", + "color-string": "^2.1.3" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/color-string": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/color-string/-/color-string-2.1.4.tgz", + "integrity": "sha512-Bb6Cq8oq0IjDOe8wJmi4JeNn763Xs9cfrBcaylK1tPypWzyoy2G3l90v9k64kjphl/ZJjPIShFztenRomi8WTg==", + "license": "MIT", + "dependencies": { + "color-name": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/color-string/node_modules/color-name": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-2.1.0.tgz", + "integrity": "sha512-1bPaDNFm0axzE4MEAzKPuqKWeRaT43U/hyxKPBdqTfmPF+d6n7FSoTFxLVULUJOmiLp01KjhIPPH+HrXZJN4Rg==", + "license": "MIT", + "engines": { + "node": ">=12.20" + } + }, + "node_modules/color/node_modules/color-convert": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-3.1.3.tgz", + "integrity": "sha512-fasDH2ont2GqF5HpyO4w0+BcewlhHEZOFn9c1ckZdHpJ56Qb7MHhH/IcJZbBGgvdtwdwNbLvxiBEdg336iA9Sg==", + "license": "MIT", + "dependencies": { + "color-name": "^2.0.0" + }, + "engines": { + "node": ">=14.6" + } + }, + "node_modules/color/node_modules/color-name": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-2.1.0.tgz", + "integrity": "sha512-1bPaDNFm0axzE4MEAzKPuqKWeRaT43U/hyxKPBdqTfmPF+d6n7FSoTFxLVULUJOmiLp01KjhIPPH+HrXZJN4Rg==", + "license": "MIT", + "engines": { + "node": ">=12.20" + } + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dev": true, + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/component-emitter": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.1.tgz", + "integrity": "sha512-T0+barUSQRTUQASh8bx02dl+DhF54GtIDY13Y3m9oWTklKbb3Wv974meRpeZ3lp1JpLVECWWNHC4vaG2XHXouQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/compressible": { + "version": "2.0.18", + "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz", + "integrity": "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==", + "license": "MIT", + "dependencies": { + "mime-db": ">= 1.43.0 < 2" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/compression": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/compression/-/compression-1.8.1.tgz", + "integrity": "sha512-9mAqGPHLakhCLeNyxPkK4xVo746zQ/czLH1Ky+vkitMnWfWZps8r0qXuwhwizagCRttsL4lfG4pIOvaWLpAP0w==", + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "compressible": "~2.0.18", + "debug": "2.6.9", + "negotiator": "~0.6.4", + "on-headers": "~1.1.0", + "safe-buffer": "5.2.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/concat-stream": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz", + "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==", + "engines": [ + "node >= 0.8" + ], + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^2.2.2", + "typedarray": "^0.0.6" + } + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz", + "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==", + "license": "MIT" + }, + "node_modules/cookiejar": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/cookiejar/-/cookiejar-2.1.4.tgz", + "integrity": "sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw==", + "dev": true, + "license": "MIT" + }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "license": "MIT" + }, + "node_modules/cors": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/create-jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/create-jest/-/create-jest-29.7.0.tgz", + "integrity": "sha512-Adz2bdH0Vq3F53KEMJOoftQFutWCukm6J24wbPWRO4k1kMY7gS7ds/uoJkNuV8wDCtWWnuwGcJwpWcih+zEW1Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.9", + "jest-config": "^29.7.0", + "jest-util": "^29.7.0", + "prompts": "^2.0.1" + }, + "bin": { + "create-jest": "bin/create-jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/dedent": { + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.7.1.tgz", + "integrity": "sha512-9JmrhGZpOlEgOLdQgSm0zxFaYoQon408V1v49aqTWuXENVlnCuY9JBZcXZiCsZQWDjTm5Qf/nIvAy77mXDAjEg==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "babel-plugin-macros": "^3.1.0" + }, + "peerDependenciesMeta": { + "babel-plugin-macros": { + "optional": true + } + } + }, + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "license": "MIT", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/detect-newline": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", + "integrity": "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/dezalgo": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/dezalgo/-/dezalgo-1.0.4.tgz", + "integrity": "sha512-rXSP0bf+5n0Qonsb+SVVfNfIsimO4HEtmnIpPHY8Q1UCzKlQrDMfdobr8nJOOsRgWCyMRqeSBQzmWUMq7zvVig==", + "dev": true, + "license": "ISC", + "dependencies": { + "asap": "^2.0.0", + "wrappy": "1" + } + }, + "node_modules/diff-sequences": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", + "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/dotenv": { + "version": "16.6.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", + "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dottie": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/dottie/-/dottie-2.0.6.tgz", + "integrity": "sha512-iGCHkfUc5kFekGiqhe8B/mdaurD+lakO9txNnTvKtA6PISrw86LgqHvRzWYPyoE2Ph5aMIrCw9/uko6XHTKCwA==", + "license": "MIT" + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/electron-to-chromium": { + "version": "1.5.267", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.267.tgz", + "integrity": "sha512-0Drusm6MVRXSOJpGbaSVgcQsuB4hEkMpHXaVstcPmhu5LIedxs1xNK/nIxmQIU/RPC0+1/o0AVZfBTkTNJOdUw==", + "dev": true, + "license": "ISC" + }, + "node_modules/emittery": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.13.1.tgz", + "integrity": "sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sindresorhus/emittery?sponsor=1" + } + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/enabled": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/enabled/-/enabled-2.0.0.tgz", + "integrity": "sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ==", + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/error-ex": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", + "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/escape-string-regexp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", + "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true, + "license": "BSD-2-Clause", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/exit": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", + "integrity": "sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==", + "dev": true, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/expect": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/expect/-/expect-29.7.0.tgz", + "integrity": "sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/expect-utils": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/express": { + "version": "4.22.1", + "resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz", + "integrity": "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==", + "license": "MIT", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "~1.20.3", + "content-disposition": "~0.5.4", + "content-type": "~1.0.4", + "cookie": "~0.7.1", + "cookie-signature": "~1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "~1.3.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.0", + "merge-descriptors": "1.0.3", + "methods": "~1.1.2", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "~0.1.12", + "proxy-addr": "~2.0.7", + "qs": "~6.14.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "~0.19.0", + "serve-static": "~1.16.2", + "setprototypeof": "1.2.0", + "statuses": "~2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express-rate-limit": { + "version": "7.5.1", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-7.5.1.tgz", + "integrity": "sha512-7iN8iPMDzOMHPUYllBEsQdWVB6fPDMPqwjBaFrgr4Jgr/+okjvzAy+UHlYYL/Vs0OsOrMkwS6PJDkFlJwoxUnw==", + "license": "MIT", + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/express-rate-limit" + }, + "peerDependencies": { + "express": ">= 4.11" + } + }, + "node_modules/express-validator": { + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/express-validator/-/express-validator-7.3.1.tgz", + "integrity": "sha512-IGenaSf+DnWc69lKuqlRE9/i/2t5/16VpH5bXoqdxWz1aCpRvEdrBuu1y95i/iL5QP8ZYVATiwLFhwk3EDl5vg==", + "license": "MIT", + "dependencies": { + "lodash": "^4.17.21", + "validator": "~13.15.23" + }, + "engines": { + "node": ">= 8.0.0" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-safe-stringify": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz", + "integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==", + "dev": true, + "license": "MIT" + }, + "node_modules/fb-watchman": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz", + "integrity": "sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "bser": "2.1.1" + } + }, + "node_modules/fecha": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/fecha/-/fecha-4.2.3.tgz", + "integrity": "sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw==", + "license": "MIT" + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/finalhandler": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz", + "integrity": "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "statuses": "~2.0.2", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/fn.name": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fn.name/-/fn.name-1.1.0.tgz", + "integrity": "sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==", + "license": "MIT" + }, + "node_modules/form-data": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "dev": true, + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/formidable": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/formidable/-/formidable-2.1.5.tgz", + "integrity": "sha512-Oz5Hwvwak/DCaXVVUtPn4oLMLLy1CdclLKO1LFgU7XzDpVMUU5UjlSLpGMocyQNNk8F6IJW9M/YdooSn2MRI+Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@paralleldrive/cuid2": "^2.2.2", + "dezalgo": "^1.0.4", + "once": "^1.4.0", + "qs": "^6.11.0" + }, + "funding": { + "url": "https://ko-fi.com/tunnckoCore/commissions" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true, + "license": "ISC" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-package-type": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", + "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/helmet": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/helmet/-/helmet-7.2.0.tgz", + "integrity": "sha512-ZRiwvN089JfMXokizgqEPXsl2Guk094yExfoDXR0cBYWxtBbaSww/w+vT4WEJsBW2iTUi1GgZ6swmoug3Oy4Xw==", + "license": "MIT", + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true, + "license": "MIT" + }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/human-signals": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=10.17.0" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ignore-by-default": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz", + "integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==", + "dev": true, + "license": "ISC" + }, + "node_modules/import-local": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.2.0.tgz", + "integrity": "sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pkg-dir": "^4.2.0", + "resolve-cwd": "^3.0.0" + }, + "bin": { + "import-local-fixture": "fixtures/cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/inflection": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/inflection/-/inflection-1.13.4.tgz", + "integrity": "sha512-6I/HUDeYFfuNCVS3td055BaXBwKYuzw7K3ExVMStBowKo9oOAMJIXIHvdyR3iboTCp1b+1i5DSkIZTcwIktuDw==", + "engines": [ + "node >= 0.4.0" + ], + "license": "MIT" + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dev": true, + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "dev": true, + "license": "MIT" + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-generator-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-generator-fn/-/is-generator-fn-2.1.0.tgz", + "integrity": "sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "license": "MIT" + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-instrument": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.3.tgz", + "integrity": "sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@babel/core": "^7.23.9", + "@babel/parser": "^7.23.9", + "@istanbuljs/schema": "^0.1.3", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-instrument/node_modules/semver": { + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-source-maps": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz", + "integrity": "sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0", + "source-map": "^0.6.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-source-maps/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/istanbul-lib-source-maps/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/istanbul-reports": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz", + "integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest/-/jest-29.7.0.tgz", + "integrity": "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/core": "^29.7.0", + "@jest/types": "^29.6.3", + "import-local": "^3.0.2", + "jest-cli": "^29.7.0" + }, + "bin": { + "jest": "bin/jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/jest-changed-files": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-29.7.0.tgz", + "integrity": "sha512-fEArFiwf1BpQ+4bXSprcDc3/x4HSzL4al2tozwVpDFpsxALjLYdyiIK4e5Vz66GQJIbXJ82+35PtysofptNX2w==", + "dev": true, + "license": "MIT", + "dependencies": { + "execa": "^5.0.0", + "jest-util": "^29.7.0", + "p-limit": "^3.1.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-circus": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-29.7.0.tgz", + "integrity": "sha512-3E1nCMgipcTkCocFwM90XXQab9bS+GMsjdpmPrlelaxwD93Ad8iVEjX/vvHPdLPnFf+L40u+5+iutRdA1N9myw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/expect": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "co": "^4.6.0", + "dedent": "^1.0.0", + "is-generator-fn": "^2.0.0", + "jest-each": "^29.7.0", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "p-limit": "^3.1.0", + "pretty-format": "^29.7.0", + "pure-rand": "^6.0.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-cli": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-29.7.0.tgz", + "integrity": "sha512-OVVobw2IubN/GSYsxETi+gOe7Ka59EFMR/twOU3Jb2GnKKeMGJB5SGUUrEz3SFVmJASUdZUzy83sLNNQ2gZslg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/core": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "create-jest": "^29.7.0", + "exit": "^0.1.2", + "import-local": "^3.0.2", + "jest-config": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "yargs": "^17.3.1" + }, + "bin": { + "jest": "bin/jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/jest-config": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-29.7.0.tgz", + "integrity": "sha512-uXbpfeQ7R6TZBqI3/TxCU4q4ttk3u0PJeC+E0zbfSoSjq6bJ7buBPxzQPL0ifrkY4DNu4JUdk0ImlBUYi840eQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.11.6", + "@jest/test-sequencer": "^29.7.0", + "@jest/types": "^29.6.3", + "babel-jest": "^29.7.0", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "deepmerge": "^4.2.2", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "jest-circus": "^29.7.0", + "jest-environment-node": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-runner": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "micromatch": "^4.0.4", + "parse-json": "^5.2.0", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@types/node": "*", + "ts-node": ">=9.0.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "ts-node": { + "optional": true + } + } + }, + "node_modules/jest-diff": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.7.0.tgz", + "integrity": "sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.0.0", + "diff-sequences": "^29.6.3", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-docblock": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-29.7.0.tgz", + "integrity": "sha512-q617Auw3A612guyaFgsbFeYpNP5t2aoUNLwBUbc/0kD1R4t9ixDbyFTHd1nok4epoVFpr7PmeWHrhvuV3XaJ4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "detect-newline": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-each": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-29.7.0.tgz", + "integrity": "sha512-gns+Er14+ZrEoC5fhOfYCY1LOHHr0TI+rQUHZS8Ttw2l7gl+80eHc/gFf2Ktkw0+SIACDTeWvpFcv3B04VembQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "jest-get-type": "^29.6.3", + "jest-util": "^29.7.0", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-environment-node": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-29.7.0.tgz", + "integrity": "sha512-DOSwCRqXirTOyheM+4d5YZOrWcdu0LNZ87ewUoywbcb2XR4wKgqiG8vNeYwhjFMbEkfju7wx2GYH0P2gevGvFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/fake-timers": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-mock": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-get-type": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz", + "integrity": "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-haste-map": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-29.7.0.tgz", + "integrity": "sha512-fP8u2pyfqx0K1rGn1R9pyE0/KTn+G7PxktWidOBTqFPLYX0b9ksaMFkhK5vrS3DVun09pckLdlx90QthlW7AmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/graceful-fs": "^4.1.3", + "@types/node": "*", + "anymatch": "^3.0.3", + "fb-watchman": "^2.0.0", + "graceful-fs": "^4.2.9", + "jest-regex-util": "^29.6.3", + "jest-util": "^29.7.0", + "jest-worker": "^29.7.0", + "micromatch": "^4.0.4", + "walker": "^1.0.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "optionalDependencies": { + "fsevents": "^2.3.2" + } + }, + "node_modules/jest-leak-detector": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-29.7.0.tgz", + "integrity": "sha512-kYA8IJcSYtST2BY9I+SMC32nDpBT3J2NvWJx8+JCuCdl/CR1I4EKUJROiP8XtCcxqgTTBGJNdbB1A8XRKbTetw==", + "dev": true, + "license": "MIT", + "dependencies": { + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-matcher-utils": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-29.7.0.tgz", + "integrity": "sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.0.0", + "jest-diff": "^29.7.0", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-message-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.7.0.tgz", + "integrity": "sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.12.13", + "@jest/types": "^29.6.3", + "@types/stack-utils": "^2.0.0", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "micromatch": "^4.0.4", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-mock": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-29.7.0.tgz", + "integrity": "sha512-ITOMZn+UkYS4ZFh83xYAOzWStloNzJFO2s8DWrE4lhtGD+AorgnbkiKERe4wQVBydIGPx059g6riW5Btp6Llnw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-pnp-resolver": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz", + "integrity": "sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "peerDependencies": { + "jest-resolve": "*" + }, + "peerDependenciesMeta": { + "jest-resolve": { + "optional": true + } + } + }, + "node_modules/jest-regex-util": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-29.6.3.tgz", + "integrity": "sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-resolve": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-29.7.0.tgz", + "integrity": "sha512-IOVhZSrg+UvVAshDSDtHyFCCBUl/Q3AAJv8iZ6ZjnZ74xzvwuzLXid9IIIPgTnY62SJjfuupMKZsZQRsCvxEgA==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-pnp-resolver": "^1.2.2", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "resolve": "^1.20.0", + "resolve.exports": "^2.0.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-resolve-dependencies": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-29.7.0.tgz", + "integrity": "sha512-un0zD/6qxJ+S0et7WxeI3H5XSe9lTBBR7bOHCHXkKR6luG5mwDDlIzVQ0V5cZCuoTgEdcdwzTghYkTWfubi+nA==", + "dev": true, + "license": "MIT", + "dependencies": { + "jest-regex-util": "^29.6.3", + "jest-snapshot": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-runner": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-29.7.0.tgz", + "integrity": "sha512-fsc4N6cPCAahybGBfTRcq5wFR6fpLznMg47sY5aDpsoejOcVYFb07AHuSnR0liMcPTgBsA3ZJL6kFOjPdoNipQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/environment": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "emittery": "^0.13.1", + "graceful-fs": "^4.2.9", + "jest-docblock": "^29.7.0", + "jest-environment-node": "^29.7.0", + "jest-haste-map": "^29.7.0", + "jest-leak-detector": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-resolve": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-util": "^29.7.0", + "jest-watcher": "^29.7.0", + "jest-worker": "^29.7.0", + "p-limit": "^3.1.0", + "source-map-support": "0.5.13" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-runtime": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-29.7.0.tgz", + "integrity": "sha512-gUnLjgwdGqW7B4LvOIkbKs9WGbn+QLqRQQ9juC6HndeDiezIwhDP+mhMwHWCEcfQ5RUXa6OPnFF8BJh5xegwwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/fake-timers": "^29.7.0", + "@jest/globals": "^29.7.0", + "@jest/source-map": "^29.6.3", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "cjs-module-lexer": "^1.0.0", + "collect-v8-coverage": "^1.0.0", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-mock": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "slash": "^3.0.0", + "strip-bom": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-snapshot": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-29.7.0.tgz", + "integrity": "sha512-Rm0BMWtxBcioHr1/OX5YCP8Uov4riHvKPknOGs804Zg9JGZgmIBkbtlxJC/7Z4msKYVbIJtfU+tKb8xlYNfdkw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.11.6", + "@babel/generator": "^7.7.2", + "@babel/plugin-syntax-jsx": "^7.7.2", + "@babel/plugin-syntax-typescript": "^7.7.2", + "@babel/types": "^7.3.3", + "@jest/expect-utils": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "babel-preset-current-node-syntax": "^1.0.0", + "chalk": "^4.0.0", + "expect": "^29.7.0", + "graceful-fs": "^4.2.9", + "jest-diff": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "natural-compare": "^1.4.0", + "pretty-format": "^29.7.0", + "semver": "^7.5.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-snapshot/node_modules/semver": { + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/jest-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", + "integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "graceful-fs": "^4.2.9", + "picomatch": "^2.2.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-validate": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-29.7.0.tgz", + "integrity": "sha512-ZB7wHqaRGVw/9hST/OuFUReG7M8vKeq0/J2egIGLdvjHCmYqGARhzXmtgi+gVeZ5uXFF219aOc3Ls2yLg27tkw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "camelcase": "^6.2.0", + "chalk": "^4.0.0", + "jest-get-type": "^29.6.3", + "leven": "^3.1.0", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-validate/node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/jest-watcher": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-29.7.0.tgz", + "integrity": "sha512-49Fg7WXkU3Vl2h6LbLtMQ/HyB6rXSIX7SqvBLQmssRBGN9I0PNvPmAmCWSOY6SOvrjhI/F7/bGAv9RtnsPA03g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "ansi-escapes": "^4.2.1", + "chalk": "^4.0.0", + "emittery": "^0.13.1", + "jest-util": "^29.7.0", + "string-length": "^4.0.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-worker": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-29.7.0.tgz", + "integrity": "sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "jest-util": "^29.7.0", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-worker/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "3.14.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz", + "integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true, + "license": "MIT" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/jsonwebtoken": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.3.tgz", + "integrity": "sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==", + "license": "MIT", + "dependencies": { + "jws": "^4.0.1", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + } + }, + "node_modules/jsonwebtoken/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/jsonwebtoken/node_modules/semver": { + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/jwa": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz", + "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==", + "license": "MIT", + "dependencies": { + "buffer-equal-constant-time": "^1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz", + "integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==", + "license": "MIT", + "dependencies": { + "jwa": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/kleur": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", + "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/kuler": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/kuler/-/kuler-2.0.0.tgz", + "integrity": "sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A==", + "license": "MIT" + }, + "node_modules/leven": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", + "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true, + "license": "MIT" + }, + "node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "license": "MIT" + }, + "node_modules/lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==", + "license": "MIT" + }, + "node_modules/lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==", + "license": "MIT" + }, + "node_modules/lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==", + "license": "MIT" + }, + "node_modules/lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==", + "license": "MIT" + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", + "license": "MIT" + }, + "node_modules/lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==", + "license": "MIT" + }, + "node_modules/lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", + "license": "MIT" + }, + "node_modules/logform": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/logform/-/logform-2.7.0.tgz", + "integrity": "sha512-TFYA4jnP7PVbmlBIfhlSe+WKxs9dklXMTEGcBCIvLhE/Tn3H6Gk1norupVW7m5Cnd4bLcr08AytbyV/xj7f/kQ==", + "license": "MIT", + "dependencies": { + "@colors/colors": "1.6.0", + "@types/triple-beam": "^1.3.2", + "fecha": "^4.2.0", + "ms": "^2.1.1", + "safe-stable-stringify": "^2.3.1", + "triple-beam": "^1.3.0" + }, + "engines": { + "node": ">= 12.0.0" + } + }, + "node_modules/logform/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/make-dir/node_modules/semver": { + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/makeerror": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz", + "integrity": "sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "tmpl": "1.0.5" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true, + "license": "MIT" + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types/node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/mkdirp": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "license": "MIT", + "dependencies": { + "minimist": "^1.2.6" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, + "node_modules/moment": { + "version": "2.30.1", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz", + "integrity": "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==", + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/moment-timezone": { + "version": "0.5.48", + "resolved": "https://registry.npmjs.org/moment-timezone/-/moment-timezone-0.5.48.tgz", + "integrity": "sha512-f22b8LV1gbTO2ms2j2z13MuPogNoh5UzxL3nzNAYKGraILnbGc9NEE6dyiiiLv46DGRb8A4kg8UKWLjPthxBHw==", + "license": "MIT", + "dependencies": { + "moment": "^2.29.4" + }, + "engines": { + "node": "*" + } + }, + "node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/multer": { + "version": "1.4.5-lts.2", + "resolved": "https://registry.npmjs.org/multer/-/multer-1.4.5-lts.2.tgz", + "integrity": "sha512-VzGiVigcG9zUAoCNU+xShztrlr1auZOlurXynNvO9GiWD1/mTBbUljOKY+qMeazBqXgRnjzeEgJI/wyjJUHg9A==", + "deprecated": "Multer 1.x is impacted by a number of vulnerabilities, which have been patched in 2.x. You should upgrade to the latest 2.x version.", + "license": "MIT", + "dependencies": { + "append-field": "^1.0.0", + "busboy": "^1.0.0", + "concat-stream": "^1.5.2", + "mkdirp": "^0.5.4", + "object-assign": "^4.1.1", + "type-is": "^1.6.4", + "xtend": "^4.0.0" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/negotiator": { + "version": "0.6.4", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.4.tgz", + "integrity": "sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/node-int64": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", + "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-releases": { + "version": "2.0.27", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", + "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nodemailer": { + "version": "6.10.1", + "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.10.1.tgz", + "integrity": "sha512-Z+iLaBGVaSjbIzQ4pX6XV41HrooLsQ10ZWPUehGmuantvzWoDVBnmsdUcOIDM1t+yPor5pDhVlDESgOMEGxhHA==", + "license": "MIT-0", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/nodemon": { + "version": "3.1.11", + "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.11.tgz", + "integrity": "sha512-is96t8F/1//UHAjNPHpbsNY46ELPpftGUoSVNXwUfMk/qdjSylYrWSu1XavVTBOn526kFiOR733ATgNBCQyH0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "chokidar": "^3.5.2", + "debug": "^4", + "ignore-by-default": "^1.0.1", + "minimatch": "^3.1.2", + "pstree.remy": "^1.1.8", + "semver": "^7.5.3", + "simple-update-notifier": "^2.0.0", + "supports-color": "^5.5.0", + "touch": "^3.1.0", + "undefsafe": "^2.0.5" + }, + "bin": { + "nodemon": "bin/nodemon.js" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/nodemon" + } + }, + "node_modules/nodemon/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/nodemon/node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/nodemon/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nodemon/node_modules/semver": { + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/nodemon/node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/on-headers": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.1.0.tgz", + "integrity": "sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/one-time": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/one-time/-/one-time-1.0.0.tgz", + "integrity": "sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g==", + "license": "MIT", + "dependencies": { + "fn.name": "1.x.x" + } + }, + "node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/p-locate/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, + "license": "MIT" + }, + "node_modules/path-to-regexp": { + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", + "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", + "license": "MIT" + }, + "node_modules/pg": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/pg/-/pg-8.17.1.tgz", + "integrity": "sha512-EIR+jXdYNSMOrpRp7g6WgQr7SaZNZfS7IzZIO0oTNEeibq956JxeD15t3Jk3zZH0KH8DmOIx38qJfQenoE8bXQ==", + "license": "MIT", + "dependencies": { + "pg-connection-string": "^2.10.0", + "pg-pool": "^3.11.0", + "pg-protocol": "^1.11.0", + "pg-types": "2.2.0", + "pgpass": "1.0.5" + }, + "engines": { + "node": ">= 16.0.0" + }, + "optionalDependencies": { + "pg-cloudflare": "^1.3.0" + }, + "peerDependencies": { + "pg-native": ">=3.0.1" + }, + "peerDependenciesMeta": { + "pg-native": { + "optional": true + } + } + }, + "node_modules/pg-cloudflare": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.3.0.tgz", + "integrity": "sha512-6lswVVSztmHiRtD6I8hw4qP/nDm1EJbKMRhf3HCYaqud7frGysPv7FYJ5noZQdhQtN2xJnimfMtvQq21pdbzyQ==", + "license": "MIT", + "optional": true + }, + "node_modules/pg-connection-string": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.10.0.tgz", + "integrity": "sha512-ur/eoPKzDx2IjPaYyXS6Y8NSblxM7X64deV2ObV57vhjsWiwLvUD6meukAzogiOsu60GO8m/3Cb6FdJsWNjwXg==", + "license": "MIT" + }, + "node_modules/pg-hstore": { + "version": "2.3.4", + "resolved": "https://registry.npmjs.org/pg-hstore/-/pg-hstore-2.3.4.tgz", + "integrity": "sha512-N3SGs/Rf+xA1M2/n0JBiXFDVMzdekwLZLAO0g7mpDY9ouX+fDI7jS6kTq3JujmYbtNSJ53TJ0q4G98KVZSM4EA==", + "license": "MIT", + "dependencies": { + "underscore": "^1.13.1" + }, + "engines": { + "node": ">= 0.8.x" + } + }, + "node_modules/pg-int8": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz", + "integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==", + "license": "ISC", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/pg-pool": { + "version": "3.11.0", + "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.11.0.tgz", + "integrity": "sha512-MJYfvHwtGp870aeusDh+hg9apvOe2zmpZJpyt+BMtzUWlVqbhFmMK6bOBXLBUPd7iRtIF9fZplDc7KrPN3PN7w==", + "license": "MIT", + "peerDependencies": { + "pg": ">=8.0" + } + }, + "node_modules/pg-protocol": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.11.0.tgz", + "integrity": "sha512-pfsxk2M9M3BuGgDOfuy37VNRRX3jmKgMjcvAcWqNDpZSf4cUmv8HSOl5ViRQFsfARFn0KuUQTgLxVMbNq5NW3g==", + "license": "MIT" + }, + "node_modules/pg-types": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz", + "integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==", + "license": "MIT", + "dependencies": { + "pg-int8": "1.0.1", + "postgres-array": "~2.0.0", + "postgres-bytea": "~1.0.0", + "postgres-date": "~1.0.4", + "postgres-interval": "^1.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/pgpass": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.5.tgz", + "integrity": "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==", + "license": "MIT", + "dependencies": { + "split2": "^4.1.0" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pirates": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", + "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "find-up": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/postgres-array": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz", + "integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/postgres-bytea": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.1.tgz", + "integrity": "sha512-5+5HqXnsZPE65IJZSMkZtURARZelel2oXUEO8rH83VS/hxH5vv1uHquPg5wZs8yMAfdv971IU+kcPUczi7NVBQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-date": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz", + "integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-interval": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz", + "integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==", + "license": "MIT", + "dependencies": { + "xtend": "^4.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "license": "MIT" + }, + "node_modules/prompts": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", + "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "kleur": "^3.0.3", + "sisteransi": "^1.0.5" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/pstree.remy": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz", + "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==", + "dev": true, + "license": "MIT" + }, + "node_modules/pure-rand": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.1.0.tgz", + "integrity": "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ], + "license": "MIT" + }, + "node_modules/qs": { + "version": "6.14.1", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.1.tgz", + "integrity": "sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.3", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz", + "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true, + "license": "MIT" + }, + "node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/readable-stream/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve": { + "version": "1.22.11", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz", + "integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-core-module": "^2.16.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-cwd": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", + "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve.exports": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/resolve.exports/-/resolve.exports-2.0.3.tgz", + "integrity": "sha512-OcXjMsGdhL4XnbShKpAcSqPMzQoYkYyhbEaeSko47MjRP9NfEQMhZkXL1DoFlt9LWQn4YttrdnV6X2OiyzBi+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/retry-as-promised": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/retry-as-promised/-/retry-as-promised-7.1.1.tgz", + "integrity": "sha512-hMD7odLOt3LkTjcif8aRZqi/hybjpLNgSk5oF5FCowfCjok6LukpN2bDX7R5wDmbgBQFn7YoBxSagmtXHaJYJw==", + "license": "MIT" + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safe-stable-stringify": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz", + "integrity": "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/send": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.2.tgz", + "integrity": "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.1", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "~2.4.1", + "range-parser": "~1.2.1", + "statuses": "~2.0.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/sequelize": { + "version": "6.37.7", + "resolved": "https://registry.npmjs.org/sequelize/-/sequelize-6.37.7.tgz", + "integrity": "sha512-mCnh83zuz7kQxxJirtFD7q6Huy6liPanI67BSlbzSYgVNl5eXVdE2CN1FuAeZwG1SNpGsNRCV+bJAVVnykZAFA==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/sequelize" + } + ], + "license": "MIT", + "dependencies": { + "@types/debug": "^4.1.8", + "@types/validator": "^13.7.17", + "debug": "^4.3.4", + "dottie": "^2.0.6", + "inflection": "^1.13.4", + "lodash": "^4.17.21", + "moment": "^2.29.4", + "moment-timezone": "^0.5.43", + "pg-connection-string": "^2.6.1", + "retry-as-promised": "^7.0.4", + "semver": "^7.5.4", + "sequelize-pool": "^7.1.0", + "toposort-class": "^1.0.1", + "uuid": "^8.3.2", + "validator": "^13.9.0", + "wkx": "^0.5.0" + }, + "engines": { + "node": ">=10.0.0" + }, + "peerDependenciesMeta": { + "ibm_db": { + "optional": true + }, + "mariadb": { + "optional": true + }, + "mysql2": { + "optional": true + }, + "oracledb": { + "optional": true + }, + "pg": { + "optional": true + }, + "pg-hstore": { + "optional": true + }, + "snowflake-sdk": { + "optional": true + }, + "sqlite3": { + "optional": true + }, + "tedious": { + "optional": true + } + } + }, + "node_modules/sequelize-pool": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/sequelize-pool/-/sequelize-pool-7.1.0.tgz", + "integrity": "sha512-G9c0qlIWQSK29pR/5U2JF5dDQeqqHRragoyahj/Nx4KOOQ3CPPfzxnfqFPCSB7x5UgjOgnZ61nSxz+fjDpRlJg==", + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/sequelize/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/sequelize/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/sequelize/node_modules/semver": { + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/sequelize/node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/serve-static": { + "version": "1.16.3", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.3.tgz", + "integrity": "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==", + "license": "MIT", + "dependencies": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "~0.19.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/simple-update-notifier": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz", + "integrity": "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/simple-update-notifier/node_modules/semver": { + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/sisteransi": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", + "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", + "dev": true, + "license": "MIT" + }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.13", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.13.tgz", + "integrity": "sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "license": "ISC", + "engines": { + "node": ">= 10.x" + } + }, + "node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/stack-trace": { + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.10.tgz", + "integrity": "sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg==", + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/stack-utils": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", + "integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "escape-string-regexp": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/streamsearch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", + "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/string_decoder/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, + "node_modules/string-length": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", + "integrity": "sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "char-regex": "^1.0.2", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-bom": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", + "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/superagent": { + "version": "8.1.2", + "resolved": "https://registry.npmjs.org/superagent/-/superagent-8.1.2.tgz", + "integrity": "sha512-6WTxW1EB6yCxV5VFOIPQruWGHqc3yI7hEmZK6h+pyk69Lk/Ut7rLUY6W/ONF2MjBuGjvmMiIpsrVJ2vjrHlslA==", + "deprecated": "Please upgrade to superagent v10.2.2+, see release notes at https://github.com/forwardemail/superagent/releases/tag/v10.2.2 - maintenance is supported by Forward Email @ https://forwardemail.net", + "dev": true, + "license": "MIT", + "dependencies": { + "component-emitter": "^1.3.0", + "cookiejar": "^2.1.4", + "debug": "^4.3.4", + "fast-safe-stringify": "^2.1.1", + "form-data": "^4.0.0", + "formidable": "^2.1.2", + "methods": "^1.1.2", + "mime": "2.6.0", + "qs": "^6.11.0", + "semver": "^7.3.8" + }, + "engines": { + "node": ">=6.4.0 <13 || >=14" + } + }, + "node_modules/superagent/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/superagent/node_modules/mime": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz", + "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==", + "dev": true, + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/superagent/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/superagent/node_modules/semver": { + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/supertest": { + "version": "6.3.4", + "resolved": "https://registry.npmjs.org/supertest/-/supertest-6.3.4.tgz", + "integrity": "sha512-erY3HFDG0dPnhw4U+udPfrzXa4xhSG+n4rxfRuZWCUvjFWwKl+OxWf/7zk50s84/fAAs7vf5QAb9uRa0cCykxw==", + "deprecated": "Please upgrade to supertest v7.1.3+, see release notes at https://github.com/forwardemail/supertest/releases/tag/v7.1.3 - maintenance is supported by Forward Email @ https://forwardemail.net", + "dev": true, + "license": "MIT", + "dependencies": { + "methods": "^1.1.2", + "superagent": "^8.1.2" + }, + "engines": { + "node": ">=6.4.0" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/test-exclude": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", + "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", + "dev": true, + "license": "ISC", + "dependencies": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^7.1.4", + "minimatch": "^3.0.4" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/text-hex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/text-hex/-/text-hex-1.0.0.tgz", + "integrity": "sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==", + "license": "MIT" + }, + "node_modules/tmpl": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", + "integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/toposort-class": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toposort-class/-/toposort-class-1.0.1.tgz", + "integrity": "sha512-OsLcGGbYF3rMjPUf8oKktyvCiUxSbqMMS39m33MAjLTC1DVIH6x3WSt63/M77ihI09+Sdfk1AXvfhCEeUmC7mg==", + "license": "MIT" + }, + "node_modules/touch": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.1.tgz", + "integrity": "sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA==", + "dev": true, + "license": "ISC", + "bin": { + "nodetouch": "bin/nodetouch.js" + } + }, + "node_modules/triple-beam": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/triple-beam/-/triple-beam-1.4.1.tgz", + "integrity": "sha512-aZbgViZrg1QNcG+LULa7nhZpJTZSLm/mXnHXnbAbjmN5aSa0y7V+wvv6+4WaBtpISJzThKy+PIPxc1Nq1EJ9mg==", + "license": "MIT", + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/type-fest": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "license": "MIT", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/typedarray": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", + "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==", + "license": "MIT" + }, + "node_modules/undefsafe": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz", + "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==", + "dev": true, + "license": "MIT" + }, + "node_modules/underscore": { + "version": "1.13.7", + "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.7.tgz", + "integrity": "sha512-GMXzWtsc57XAtguZgaQViUOzs0KTkk8ojr3/xAxXLITqf/3EMwxC0inyETfDFjH/Krbhuep0HNbbjI9i/q3F3g==", + "license": "MIT" + }, + "node_modules/undici-types": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", + "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", + "license": "MIT" + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/v8-to-istanbul": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz", + "integrity": "sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==", + "dev": true, + "license": "ISC", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.12", + "@types/istanbul-lib-coverage": "^2.0.1", + "convert-source-map": "^2.0.0" + }, + "engines": { + "node": ">=10.12.0" + } + }, + "node_modules/validator": { + "version": "13.15.26", + "resolved": "https://registry.npmjs.org/validator/-/validator-13.15.26.tgz", + "integrity": "sha512-spH26xU080ydGggxRyR1Yhcbgx+j3y5jbNXk/8L+iRvdIEQ4uTRH2Sgf2dokud6Q4oAtsbNvJ1Ft+9xmm6IZcA==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/walker": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz", + "integrity": "sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "makeerror": "1.0.12" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/winston": { + "version": "3.19.0", + "resolved": "https://registry.npmjs.org/winston/-/winston-3.19.0.tgz", + "integrity": "sha512-LZNJgPzfKR+/J3cHkxcpHKpKKvGfDZVPS4hfJCc4cCG0CgYzvlD6yE/S3CIL/Yt91ak327YCpiF/0MyeZHEHKA==", + "license": "MIT", + "dependencies": { + "@colors/colors": "^1.6.0", + "@dabh/diagnostics": "^2.0.8", + "async": "^3.2.3", + "is-stream": "^2.0.0", + "logform": "^2.7.0", + "one-time": "^1.0.0", + "readable-stream": "^3.4.0", + "safe-stable-stringify": "^2.3.1", + "stack-trace": "0.0.x", + "triple-beam": "^1.3.0", + "winston-transport": "^4.9.0" + }, + "engines": { + "node": ">= 12.0.0" + } + }, + "node_modules/winston-transport": { + "version": "4.9.0", + "resolved": "https://registry.npmjs.org/winston-transport/-/winston-transport-4.9.0.tgz", + "integrity": "sha512-8drMJ4rkgaPo1Me4zD/3WLfI/zPdA9o2IipKODunnGDcuqbHwjsbB79ylv04LCGGzU0xQ6vTznOMpQGaLhhm6A==", + "license": "MIT", + "dependencies": { + "logform": "^2.7.0", + "readable-stream": "^3.6.2", + "triple-beam": "^1.3.0" + }, + "engines": { + "node": ">= 12.0.0" + } + }, + "node_modules/winston-transport/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/winston/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/wkx": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/wkx/-/wkx-0.5.0.tgz", + "integrity": "sha512-Xng/d4Ichh8uN4l0FToV/258EjMGU9MGcA0HV2d9B/ZpZB3lqQm7nkOdZdm5GhKtLLhAE7PiVQwN4eN+2YJJUg==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/write-file-atomic": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-4.0.2.tgz", + "integrity": "sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==", + "dev": true, + "license": "ISC", + "dependencies": { + "imurmurhash": "^0.1.4", + "signal-exit": "^3.0.7" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "license": "MIT", + "engines": { + "node": ">=0.4" + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..2be7f9e --- /dev/null +++ b/package.json @@ -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" + } +} \ No newline at end of file diff --git a/routes/applications.js b/routes/applications.js new file mode 100644 index 0000000..f310694 --- /dev/null +++ b/routes/applications.js @@ -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; diff --git a/routes/auth.js b/routes/auth.js new file mode 100644 index 0000000..abc2737 --- /dev/null +++ b/routes/auth.js @@ -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; diff --git a/routes/constitutional.js b/routes/constitutional.js new file mode 100644 index 0000000..f5800e5 --- /dev/null +++ b/routes/constitutional.js @@ -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; diff --git a/routes/finance.js b/routes/finance.js new file mode 100644 index 0000000..a0a8ece --- /dev/null +++ b/routes/finance.js @@ -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; diff --git a/routes/master.js b/routes/master.js new file mode 100644 index 0000000..c5d3d12 --- /dev/null +++ b/routes/master.js @@ -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; diff --git a/routes/outlets.js b/routes/outlets.js new file mode 100644 index 0000000..a4722f9 --- /dev/null +++ b/routes/outlets.js @@ -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; diff --git a/routes/relocation.js b/routes/relocation.js new file mode 100644 index 0000000..d920cb1 --- /dev/null +++ b/routes/relocation.js @@ -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; diff --git a/routes/resignations.js b/routes/resignations.js new file mode 100644 index 0000000..e2cf48f --- /dev/null +++ b/routes/resignations.js @@ -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; diff --git a/routes/upload.js b/routes/upload.js new file mode 100644 index 0000000..7e62df0 --- /dev/null +++ b/routes/upload.js @@ -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; diff --git a/routes/worknotes.js b/routes/worknotes.js new file mode 100644 index 0000000..22cab85 --- /dev/null +++ b/routes/worknotes.js @@ -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; diff --git a/scripts/migrate.js b/scripts/migrate.js new file mode 100644 index 0000000..5e06d71 --- /dev/null +++ b/scripts/migrate.js @@ -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(); diff --git a/scripts/seed.js b/scripts/seed.js new file mode 100644 index 0000000..8949fd5 --- /dev/null +++ b/scripts/seed.js @@ -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(); diff --git a/server.js b/server.js new file mode 100644 index 0000000..b644310 --- /dev/null +++ b/server.js @@ -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; + diff --git a/services/auditService.js b/services/auditService.js new file mode 100644 index 0000000..b196b72 --- /dev/null +++ b/services/auditService.js @@ -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 +}; diff --git a/src/common/config/auth.js b/src/common/config/auth.js new file mode 100644 index 0000000..c402910 --- /dev/null +++ b/src/common/config/auth.js @@ -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 +}; diff --git a/src/common/config/constants.js b/src/common/config/constants.js new file mode 100644 index 0000000..a8227b1 --- /dev/null +++ b/src/common/config/constants.js @@ -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 +}; diff --git a/src/common/config/database.js b/src/common/config/database.js new file mode 100644 index 0000000..8b9fd3b --- /dev/null +++ b/src/common/config/database.js @@ -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 + } +}; diff --git a/src/common/middleware/auth.js b/src/common/middleware/auth.js new file mode 100644 index 0000000..9ebbbf3 --- /dev/null +++ b/src/common/middleware/auth.js @@ -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 +}; diff --git a/src/common/middleware/errorHandler.js b/src/common/middleware/errorHandler.js new file mode 100644 index 0000000..6f54581 --- /dev/null +++ b/src/common/middleware/errorHandler.js @@ -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; diff --git a/src/common/middleware/roleCheck.js b/src/common/middleware/roleCheck.js new file mode 100644 index 0000000..af26545 --- /dev/null +++ b/src/common/middleware/roleCheck.js @@ -0,0 +1,47 @@ +const { ROLES } = require('../config/constants'); +const logger = require('../utils/logger'); + +/** + * Role-based access control middleware + * @param {Array} 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 +}; diff --git a/src/common/utils/logger.js b/src/common/utils/logger.js new file mode 100644 index 0000000..3506b72 --- /dev/null +++ b/src/common/utils/logger.js @@ -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; diff --git a/src/database/models/Application.js b/src/database/models/Application.js new file mode 100644 index 0000000..fadf560 --- /dev/null +++ b/src/database/models/Application.js @@ -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; +}; diff --git a/src/database/models/AuditLog.js b/src/database/models/AuditLog.js new file mode 100644 index 0000000..cf61b78 --- /dev/null +++ b/src/database/models/AuditLog.js @@ -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; +}; diff --git a/src/database/models/ConstitutionalChange.js b/src/database/models/ConstitutionalChange.js new file mode 100644 index 0000000..5f8db11 --- /dev/null +++ b/src/database/models/ConstitutionalChange.js @@ -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; +}; diff --git a/src/database/models/Document.js b/src/database/models/Document.js new file mode 100644 index 0000000..e6ff4c2 --- /dev/null +++ b/src/database/models/Document.js @@ -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; +}; diff --git a/src/database/models/FinancePayment.js b/src/database/models/FinancePayment.js new file mode 100644 index 0000000..42905ca --- /dev/null +++ b/src/database/models/FinancePayment.js @@ -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; +}; diff --git a/src/database/models/FnF.js b/src/database/models/FnF.js new file mode 100644 index 0000000..3974072 --- /dev/null +++ b/src/database/models/FnF.js @@ -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; +}; diff --git a/src/database/models/FnFLineItem.js b/src/database/models/FnFLineItem.js new file mode 100644 index 0000000..65e15cc --- /dev/null +++ b/src/database/models/FnFLineItem.js @@ -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; +}; diff --git a/src/database/models/Notification.js b/src/database/models/Notification.js new file mode 100644 index 0000000..a2cd94c --- /dev/null +++ b/src/database/models/Notification.js @@ -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; +}; diff --git a/src/database/models/Outlet.js b/src/database/models/Outlet.js new file mode 100644 index 0000000..990ec48 --- /dev/null +++ b/src/database/models/Outlet.js @@ -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; +}; diff --git a/src/database/models/Region.js b/src/database/models/Region.js new file mode 100644 index 0000000..22b91b4 --- /dev/null +++ b/src/database/models/Region.js @@ -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; +}; diff --git a/src/database/models/RelocationRequest.js b/src/database/models/RelocationRequest.js new file mode 100644 index 0000000..255d072 --- /dev/null +++ b/src/database/models/RelocationRequest.js @@ -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; +}; diff --git a/src/database/models/Resignation.js b/src/database/models/Resignation.js new file mode 100644 index 0000000..7665c83 --- /dev/null +++ b/src/database/models/Resignation.js @@ -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; +}; diff --git a/src/database/models/SLAConfiguration.js b/src/database/models/SLAConfiguration.js new file mode 100644 index 0000000..c90db2f --- /dev/null +++ b/src/database/models/SLAConfiguration.js @@ -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; +}; diff --git a/src/database/models/SLAEscalationConfig.js b/src/database/models/SLAEscalationConfig.js new file mode 100644 index 0000000..4a385ab --- /dev/null +++ b/src/database/models/SLAEscalationConfig.js @@ -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; +}; diff --git a/src/database/models/SLAReminder.js b/src/database/models/SLAReminder.js new file mode 100644 index 0000000..95a16f6 --- /dev/null +++ b/src/database/models/SLAReminder.js @@ -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; +}; diff --git a/src/database/models/User.js b/src/database/models/User.js new file mode 100644 index 0000000..f93b832 --- /dev/null +++ b/src/database/models/User.js @@ -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; +}; diff --git a/src/database/models/WorkflowStageConfig.js b/src/database/models/WorkflowStageConfig.js new file mode 100644 index 0000000..0e3b3e0 --- /dev/null +++ b/src/database/models/WorkflowStageConfig.js @@ -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; +}; diff --git a/src/database/models/Worknote.js b/src/database/models/Worknote.js new file mode 100644 index 0000000..47e47bb --- /dev/null +++ b/src/database/models/Worknote.js @@ -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; +}; diff --git a/src/database/models/Zone.js b/src/database/models/Zone.js new file mode 100644 index 0000000..a780694 --- /dev/null +++ b/src/database/models/Zone.js @@ -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; +}; diff --git a/src/database/models/index.js b/src/database/models/index.js new file mode 100644 index 0000000..80bbf57 --- /dev/null +++ b/src/database/models/index.js @@ -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; diff --git a/src/modules/auth/auth.controller.js b/src/modules/auth/auth.controller.js new file mode 100644 index 0000000..763931f --- /dev/null +++ b/src/modules/auth/auth.controller.js @@ -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' + }); + } +}; diff --git a/src/modules/auth/auth.routes.js b/src/modules/auth/auth.routes.js new file mode 100644 index 0000000..1189a6d --- /dev/null +++ b/src/modules/auth/auth.routes.js @@ -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; diff --git a/src/modules/collaboration/collaboration.controller.js b/src/modules/collaboration/collaboration.controller.js new file mode 100644 index 0000000..d91cbc1 --- /dev/null +++ b/src/modules/collaboration/collaboration.controller.js @@ -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' }); + } +}; diff --git a/src/modules/collaboration/collaboration.routes.js b/src/modules/collaboration/collaboration.routes.js new file mode 100644 index 0000000..30d00e4 --- /dev/null +++ b/src/modules/collaboration/collaboration.routes.js @@ -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; diff --git a/src/modules/master/master.controller.js b/src/modules/master/master.controller.js new file mode 100644 index 0000000..fce3049 --- /dev/null +++ b/src/modules/master/master.controller.js @@ -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' }); + } +}; diff --git a/src/modules/master/master.routes.js b/src/modules/master/master.routes.js new file mode 100644 index 0000000..7d19a1b --- /dev/null +++ b/src/modules/master/master.routes.js @@ -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; diff --git a/src/modules/master/outlet.controller.js b/src/modules/master/outlet.controller.js new file mode 100644 index 0000000..1a52da2 --- /dev/null +++ b/src/modules/master/outlet.controller.js @@ -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' + }); + } +}; diff --git a/src/modules/master/outlet.routes.js b/src/modules/master/outlet.routes.js new file mode 100644 index 0000000..a49c791 --- /dev/null +++ b/src/modules/master/outlet.routes.js @@ -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; diff --git a/src/modules/onboarding/onboarding.controller.js b/src/modules/onboarding/onboarding.controller.js new file mode 100644 index 0000000..52d811f --- /dev/null +++ b/src/modules/onboarding/onboarding.controller.js @@ -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' }); + } +}; diff --git a/src/modules/onboarding/onboarding.routes.js b/src/modules/onboarding/onboarding.routes.js new file mode 100644 index 0000000..483f80c --- /dev/null +++ b/src/modules/onboarding/onboarding.routes.js @@ -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; diff --git a/src/modules/self-service/constitutional.controller.js b/src/modules/self-service/constitutional.controller.js new file mode 100644 index 0000000..edd9349 --- /dev/null +++ b/src/modules/self-service/constitutional.controller.js @@ -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' }); + } +}; diff --git a/src/modules/self-service/relocation.controller.js b/src/modules/self-service/relocation.controller.js new file mode 100644 index 0000000..522a3b5 --- /dev/null +++ b/src/modules/self-service/relocation.controller.js @@ -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; +} diff --git a/src/modules/self-service/resignation.controller.js b/src/modules/self-service/resignation.controller.js new file mode 100644 index 0000000..77067ff --- /dev/null +++ b/src/modules/self-service/resignation.controller.js @@ -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); + } +}; diff --git a/src/modules/self-service/resignation.routes.js b/src/modules/self-service/resignation.routes.js new file mode 100644 index 0000000..f8b6cdf --- /dev/null +++ b/src/modules/self-service/resignation.routes.js @@ -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; diff --git a/src/modules/self-service/self-service.routes.js b/src/modules/self-service/self-service.routes.js new file mode 100644 index 0000000..c9d35ef --- /dev/null +++ b/src/modules/self-service/self-service.routes.js @@ -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; diff --git a/src/modules/settlement/settlement.controller.js b/src/modules/settlement/settlement.controller.js new file mode 100644 index 0000000..9eb34a1 --- /dev/null +++ b/src/modules/settlement/settlement.controller.js @@ -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' }); + } +}; diff --git a/src/modules/settlement/settlement.routes.js b/src/modules/settlement/settlement.routes.js new file mode 100644 index 0000000..37d598f --- /dev/null +++ b/src/modules/settlement/settlement.routes.js @@ -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; diff --git a/utils/logger.js b/utils/logger.js new file mode 100644 index 0000000..9d8a756 --- /dev/null +++ b/utils/logger.js @@ -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;