database script files cleaned migration table created planned for fresh setup

This commit is contained in:
Laxman 2026-05-26 18:40:55 +05:30
parent 80495a78a6
commit 41fe7963a7
61 changed files with 892 additions and 1829 deletions

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -5,8 +5,8 @@
<link rel="icon" type="image/svg+xml" href="/vite.svg" /> <link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Royal Enfield Onboarding</title> <title>Royal Enfield Onboarding</title>
<script type="module" crossorigin src="/assets/index-XdyJ-8da.js"></script> <script type="module" crossorigin src="/assets/index-CIW1_Mz_.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-DqVo88us.css"> <link rel="stylesheet" crossorigin href="/assets/index-CDNp5hMY.css">
</head> </head>
<body> <body>
<div id="root"></div> <div id="root"></div>

View File

@ -41,7 +41,7 @@ Ordered by impact. Update this file when items ship.
## Verification checklist ## Verification checklist
1. `npx tsx scripts/migrate-sla-tracking-schema.ts` (once per DB if needed) 1. `npm run migrate:up` (applies pending versioned migrations, including SLA schema)
2. `npx tsx scripts/seed-sla-configs.ts` 2. `npx tsx scripts/seed-sla-configs.ts`
3. `ENABLE_REDIS=true` + restart API 3. `ENABLE_REDIS=true` + restart API
4. Operations monitor → analytics cards, **My queue**, **Export CSV**, **Schedulers** → questionnaire settings 4. Operations monitor → analytics cards, **My queue**, **Export CSV**, **Schedulers** → questionnaire settings

View File

@ -38,19 +38,20 @@ Or use **Master → SLA Configuration → Initialize defaults** in the UI.
## DB note ## DB note
If `sla_tracking.metadata` (or `entityType` / `entityId`) is missing on an older database, run: Schema for `sla_tracking` and `sla_notification_dispatches` is defined in the
Sequelize models (`src/database/models/compliance/`). On a fresh database
`npm run migrate` builds them automatically.
For environments that already hold data, run the versioned migration runner
which only applies the migrations not yet recorded in the `migrations` table:
```bash ```bash
npx tsx scripts/migrate-sla-tracking-schema.ts npm run migrate:up
``` ```
### SLA notification dispatch log (idempotency + audit) ### SLA notification dispatch log (idempotency + audit)
Table `sla_notification_dispatches` records **one row per threshold per active track** (pre-breach reminder, breach, escalation L1L3, repeat overdue window). Unique on `(trackingId, thresholdKey)` prevents duplicate emails even if the worker runs every minute. Table `sla_notification_dispatches` records **one row per threshold per active track** (pre-breach reminder, breach, escalation L1L3, repeat overdue window). Unique on `(trackingId, thresholdKey)` prevents duplicate emails even if the worker runs every minute. Created by the model definition + the migration `scripts/migrations/20260526000000_create_sla_notification_dispatches.ts`.
```bash
npx tsx scripts/migrate-sla-notification-dispatches.ts
```
| `dispatchType` | `thresholdKey` example | Sends | | `dispatchType` | `thresholdKey` example | Sends |
|----------------|------------------------|--------| |----------------|------------------------|--------|

View File

@ -10,7 +10,10 @@
"build": "tsc", "build": "tsc",
"type-check": "tsc --noEmit", "type-check": "tsc --noEmit",
"migrate": "tsx scripts/migrate.ts", "migrate": "tsx scripts/migrate.ts",
"migrate:sla-dispatches": "tsx scripts/migrate-sla-notification-dispatches.ts", "migrate:up": "tsx scripts/run-migrations.ts",
"migrate:status": "tsx scripts/run-migrations.ts --status",
"migrate:baseline": "tsx scripts/run-migrations.ts --baseline",
"migrate:create": "tsx scripts/create-migration.ts",
"reset:stable": "tsx scripts/reset_db_stable.ts", "reset:stable": "tsx scripts/reset_db_stable.ts",
"seed": "tsx scripts/seed_normalized_data.ts", "seed": "tsx scripts/seed_normalized_data.ts",
"seed:roles": "tsx scripts/seed-roles.ts", "seed:roles": "tsx scripts/seed-roles.ts",

View File

@ -1,39 +0,0 @@
import 'dotenv/config';
import db from '../src/database/models/index.js';
const { Role } = db;
async function addArchitectureRole() {
console.log('Adding Architecture Role...');
try {
await db.sequelize.authenticate();
await Role.findOrCreate({
where: { roleCode: 'ARCHITECTURE' },
defaults: {
roleCode: 'ARCHITECTURE',
roleName: 'Architecture',
category: 'DEPARTMENT',
isActive: true
}
});
// Also add the 'Architecture' alias if needed for frontend mapping
await Role.findOrCreate({
where: { roleCode: 'Architecture' },
defaults: {
roleCode: 'Architecture',
roleName: 'Architecture Team',
category: 'DEPARTMENT',
isActive: true
}
});
console.log('✅ Architecture role added successfully!');
process.exit(0);
} catch (error) {
console.error('❌ Failed to add role:', error);
process.exit(1);
}
}
addArchitectureRole();

View File

@ -1,26 +0,0 @@
import { Sequelize } from 'sequelize';
const sequelize = new Sequelize('royal_enfield_onboarding', 'laxman', '<.efvP1D0^80Z)r5', {
host: 'localhost',
dialect: 'postgres',
logging: console.log
});
const run = async () => {
try {
await sequelize.authenticate();
console.log('Connected to database.');
console.log('Adding decision column to interview_evaluations table...');
await sequelize.query('ALTER TABLE "interview_evaluations" ADD COLUMN IF NOT EXISTS "decision" VARCHAR(255);');
console.log('Column added successfully.');
process.exit(0);
} catch (error) {
console.error('Error:', error);
process.exit(1);
}
};
run();

View File

@ -1,28 +0,0 @@
import 'dotenv/config';
import db from '../src/database/models/index.js';
const updateEnum = async () => {
try {
console.log('>>> STARTING ENUM MIGRATION (Level 3) <<<');
await db.sequelize.authenticate();
console.log('Database connection established.');
try {
await db.sequelize.query(`ALTER TYPE "enum_applications_overallStatus" ADD VALUE IF NOT EXISTS 'Level 3 Approved';`);
console.log('Added Level 3 Approved');
} catch (e) {
console.log('Level 3 Approved likely exists or error', e instanceof Error ? e.message : String(e));
}
console.log('>>> SUCCESS: Enum values updated <<<');
await db.sequelize.close();
process.exit(0);
} catch (error) {
console.error('>>> ERROR: Failed to update Enum', error);
process.exit(1);
}
};
updateEnum();

View File

@ -1,27 +0,0 @@
import 'dotenv/config';
import db from '../src/database/models/index.js';
const addRecoveryEnum = async () => {
try {
console.log('>>> STARTING ENUM UPDATE: FnFLineItem itemType <<<');
await db.sequelize.authenticate();
console.log('Database connection established.');
// Raw query to add 'Recovery' to the itemType enum
try {
await db.sequelize.query(`ALTER TYPE "enum_fnf_line_items_itemType" ADD VALUE IF NOT EXISTS 'Recovery';`);
console.log('SUCCESS: Added "Recovery" to "enum_fnf_line_items_itemType"');
} catch (e) {
console.log('NOTICE: "Recovery" likely already exists or another error occurred:', e.message);
}
await db.sequelize.close();
console.log('>>> ENUM UPDATE COMPLETED <<<');
process.exit(0);
} catch (error) {
console.error('>>> ERROR: Failed to update Enum:', error);
process.exit(1);
}
};
addRecoveryEnum();

View File

@ -1,74 +0,0 @@
import dotenv from 'dotenv';
import path from 'path';
import { fileURLToPath } from 'url';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
dotenv.config({ path: path.join(__dirname, '../.env') });
import db from '../src/database/models/index.js';
import { syncLocationManagers } from '../src/modules/master/syncHierarchy.service.js';
async function run() {
try {
// 1. Find the South Delhi district
const district = await db.District.findOne({
where: { name: { [db.Sequelize.Op.iLike]: '%South Delhi%' } }
});
if (!district) {
console.log('District "South Delhi" not found');
return;
}
console.log(`Found District: ${district.name} (${district.id})`);
// 2. Find a DD-AM user
// The role code might be 'DD AM' or 'DD-AM' based on constants
const user = await db.User.findOne({
where: {
[db.Sequelize.Op.or]: [
{ roleCode: 'DD AM' },
{ roleCode: 'DD-AM' }
],
isActive: true
}
});
if (!user) {
console.log('No active DD-AM user found');
return;
}
console.log(`Found DD-AM User: ${user.fullName} (${user.id})`);
// 3. Create/Update UserRole mapping
const [userRole, created] = await db.UserRole.findOrCreate({
where: {
userId: user.id,
districtId: district.id,
isActive: true
},
defaults: {
roleId: (await db.Role.findOne({ where: { roleCode: user.roleCode } })).id,
isPrimary: true,
isActive: true
}
});
if (created) {
console.log('Created new UserRole assignment');
} else {
console.log('UserRole assignment already exists');
}
// 4. Sync Location Managers
await syncLocationManagers(district.id);
console.log('Sync completed');
process.exit(0);
} catch (error) {
console.error('Error:', error);
process.exit(1);
}
}
run();

View File

@ -1,28 +0,0 @@
import 'dotenv/config';
import db from '../src/database/models/index.js';
async function main() {
const { sequelize, SLANotificationDispatch, SLATracking } = db;
await sequelize.authenticate();
const count = await SLANotificationDispatch.count();
const activeTracks = await SLATracking.count({ where: { isActive: true, endTime: null } });
const recent = await SLANotificationDispatch.findAll({
limit: 15,
order: [['sentAt', 'DESC']]
});
console.log('dispatches:', count, 'active tracks:', activeTracks);
for (const r of recent) {
console.log(
r.dispatchType,
r.thresholdKey,
String(r.trackingId).slice(0, 8),
r.sentAt
);
}
await sequelize.close();
}
main().catch((e) => {
console.error(e);
process.exit(1);
});

View File

@ -1,18 +0,0 @@
import 'dotenv/config';
import { initializeSmtpConfig } from '../src/services/smtpConfig.service.js';
async function main() {
const cfg = await initializeSmtpConfig();
console.log('enabled:', cfg.enabled);
console.log('source:', cfg.source);
console.log('host:', cfg.host);
console.log('port:', cfg.port);
console.log('user:', cfg.auth.user || '(empty)');
console.log('pass:', cfg.auth.pass ? `*** (${cfg.auth.pass.length})` : '(empty)');
console.log('from:', cfg.from);
}
main().catch((e) => {
console.error(e);
process.exit(1);
});

View File

@ -1,26 +0,0 @@
import db from '../src/database/models/index.js';
async function check() {
try {
const app = await (db as any).Application.findOne({
where: { email: 'test-dealer-tumkur@example.com' },
include: [{ model: (db as any).District, as: 'district' }]
});
if (app) {
console.log('Application Found:');
console.log('ID:', app.applicationId);
console.log('District Name:', app.district ? app.district.name : 'NULL');
console.log('District ID:', app.districtId);
console.log('Is Opportunity Available (Status):', app.overallStatus);
} else {
console.log('Application not found.');
}
process.exit(0);
} catch (err) {
console.error(err);
process.exit(1);
}
}
check();

View File

@ -1,18 +0,0 @@
import db from '../src/database/models/index.js';
async function checkColumn() {
try {
const [results]: any = await db.sequelize.query(`
SELECT column_name, data_type, udt_name
FROM information_schema.columns
WHERE table_name = 'request_participants' AND column_name = 'participantType'
`);
console.log('Column definition:', results[0]);
} catch (error: any) {
console.error('Error fetching column:', error.message);
} finally {
process.exit(0);
}
}
checkColumn();

View File

@ -1,19 +0,0 @@
import db from '../src/database/models/index.js';
async function checkEnum() {
try {
const [results]: any = await db.sequelize.query(`
SELECT enumlabel
FROM pg_enum
JOIN pg_type ON pg_enum.enumtypid = pg_type.oid
WHERE typname = 'enum_request_participants_participantType'
`);
console.log('Current enum values:', results.map((r: any) => r.enumlabel).join(', '));
} catch (error: any) {
console.error('Error fetching enum:', error.message);
} finally {
process.exit(0);
}
}
checkEnum();

View File

@ -1,48 +0,0 @@
import dotenv from 'dotenv';
import path from 'path';
import { fileURLToPath } from 'url';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
dotenv.config({ path: path.join(__dirname, '../.env') });
import db from '../src/database/models/index.js';
async function run() {
try {
const app = await db.Application.findOne({
order: [['updatedAt', 'DESC']],
include: [
{ model: db.District, as: 'district' },
{
model: db.RequestParticipant,
as: 'participants',
include: [{ model: db.User, as: 'user' }]
}
]
});
if (!app) {
console.log('No applications found');
return;
}
console.log('Application ID:', app.id);
console.log('Status:', app.status);
console.log('District:', app.district?.name);
console.log('District ddAmId:', app.district?.ddAmId);
console.log('District asmId:', app.district?.asmId);
console.log('Participants:');
app.participants?.forEach(p => {
console.log(`- ${p.user?.fullName} (${p.metadata?.role})`);
});
process.exit(0);
} catch (error) {
console.error('Error:', error);
process.exit(1);
}
}
run();

View File

@ -1,26 +0,0 @@
import db from '../src/database/models/index.js';
async function cleanup() {
try {
await db.sequelize.authenticate();
console.log('Database connected.');
const [results1]: any = await db.sequelize.query(`
DELETE FROM interview_participants
WHERE "interviewId" NOT IN (SELECT id FROM interviews)
`);
const [results2]: any = await db.sequelize.query(`
DELETE FROM interview_evaluations
WHERE "interviewId" NOT IN (SELECT id FROM interviews)
`);
console.log('Cleanup finished.');
process.exit(0);
} catch (err) {
console.error('Cleanup failed:', err);
process.exit(1);
}
}
cleanup();

106
scripts/create-migration.ts Normal file
View File

@ -0,0 +1,106 @@
/**
* Scaffold a new migration file under `scripts/migrations/`.
*
* Usage: npm run migrate:create -- <snake_case_description>
* e.g.: npm run migrate:create -- add_finance_kyc_column
*
* The file is named `<YYYYMMDDHHMMSS>_<description>.ts` (UTC timestamp).
* Author then edits `up()` to implement the schema change.
*/
import { promises as fs } from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
const SCRIPTS_DIR = path.dirname(fileURLToPath(import.meta.url));
const MIGRATIONS_DIR = path.join(SCRIPTS_DIR, 'migrations');
function pad(n: number, width = 2): string {
return String(n).padStart(width, '0');
}
function utcTimestamp(): string {
const d = new Date();
return (
d.getUTCFullYear().toString() +
pad(d.getUTCMonth() + 1) +
pad(d.getUTCDate()) +
pad(d.getUTCHours()) +
pad(d.getUTCMinutes()) +
pad(d.getUTCSeconds())
);
}
function sanitizeName(input: string): string {
const cleaned = input
.trim()
.toLowerCase()
.replace(/[^a-z0-9]+/g, '_')
.replace(/^_+|_+$/g, '');
if (!cleaned) {
throw new Error('Migration name is empty after sanitisation.');
}
if (cleaned.length > 80) {
throw new Error(
`Migration name "${cleaned}" is too long (${cleaned.length}). Keep it under 80 chars.`
);
}
return cleaned;
}
async function main(): Promise<void> {
const rawName = process.argv.slice(2).join('_');
if (!rawName) {
console.error('Missing migration name.');
console.error('Usage: npm run migrate:create -- <snake_case_description>');
process.exit(1);
}
const name = sanitizeName(rawName);
const ts = utcTimestamp();
const filename = `${ts}_${name}.ts`;
const target = path.join(MIGRATIONS_DIR, filename);
const body = `/**
* Migration: ${name.replace(/_/g, ' ')}
*
* Generated at ${new Date().toISOString()}.
* Implement up() with idempotent DDL where possible.
*/
import type { QueryInterface, Sequelize, Transaction } from 'sequelize';
export interface MigrationContext {
queryInterface: QueryInterface;
sequelize: Sequelize;
transaction: Transaction;
}
const migration = {
async up({ sequelize, transaction }: MigrationContext): Promise<void> {
// TODO: implement the schema change.
// Example:
// await sequelize.query(\`
// ALTER TABLE "applications"
// ADD COLUMN IF NOT EXISTS "kycReviewedAt" TIMESTAMPTZ NULL;
// \`, { transaction });
throw new Error('Migration ${filename} has no up() implementation yet.');
}
};
export default migration;
`;
await fs.mkdir(MIGRATIONS_DIR, { recursive: true });
await fs.writeFile(target, body, { flag: 'wx' });
console.log(`Created ${path.relative(process.cwd(), target)}`);
console.log('Next:');
console.log(' 1. Edit the file and implement up().');
console.log(' 2. Update the matching Sequelize model.');
console.log(' 3. Run: npm run migrate:up');
}
main().catch((err) => {
console.error('create-migration failed:', err?.message || err);
process.exit(1);
});

View File

@ -1,40 +0,0 @@
/**
* Create System Audit Log Table
*
* Bootstraps the new `system_audit_logs` table on environments where the
* full `migrate.ts` (sequelize.sync({ force: true })) cannot be run because
* the database already holds production / shared data.
*
* Safe to re-run: uses `SystemAuditLog.sync()` (no `force`, no `alter`),
* which is a no-op once the table exists.
*
* Run: npx tsx scripts/create-system-audit-log-table.ts
*/
import 'dotenv/config';
import db from '../src/database/models/index.js';
async function run() {
console.log('🔄 Ensuring system_audit_logs table exists...');
try {
await db.sequelize.authenticate();
console.log('📡 Database connection OK');
await db.SystemAuditLog.sync();
const [rows] = await db.sequelize.query(
`SELECT COUNT(*)::int AS total FROM system_audit_logs`
);
const total = (rows as any[])[0]?.total ?? 0;
console.log('✅ system_audit_logs is ready');
console.log(` Existing rows: ${total}`);
process.exit(0);
} catch (err: any) {
console.error('❌ Failed to ensure system_audit_logs table:', err.message || err);
if (err.stack) console.error(err.stack);
process.exit(1);
}
}
run();

View File

@ -1,63 +0,0 @@
import db from '../src/database/models/index.js';
async function checkAreaManager() {
try {
console.log('Connecting to database...');
await db.sequelize.authenticate();
console.log('Database connected.');
// Fetch all areas
const areas = await db.Area.findAll({
include: [
{ model: db.User, as: 'manager', attributes: ['id', 'fullName'] }
]
});
console.log(`Found ${areas.length} areas.`);
if (areas.length > 0) {
areas.forEach((area: any) => {
console.log(`Area: ${area.areaName} (${area.id})`);
console.log(` - Manager ID (Field): ${area.managerId}`);
console.log(` - Manager (Association): ${area.manager ? area.manager.fullName : 'None'}`);
console.log('-----------------------------------');
});
// Pick the first area and try to update it manually if managerId is null
const targetArea = areas[0];
// Find a user to assign (any user)
const user = await db.User.findOne();
if (user) {
console.log(`Attempting to assign User ${user.fullName} (${user.id}) to Area ${targetArea.areaName}...`);
targetArea.managerId = user.id;
await targetArea.save();
console.log('Update saved. Re-fetching to verify...');
const updatedArea = await db.Area.findByPk(targetArea.id);
console.log(`Re-fetched Area Manager ID: ${updatedArea?.managerId}`);
if (updatedArea?.managerId === user.id) {
console.log('SUCCESS: Manager ID persisted correctly.');
} else {
console.error('FAILURE: Manager ID did not persist.');
}
} else {
console.log('No users found to test assignment.');
}
} else {
console.log('No areas found.');
}
} catch (error) {
console.error('Error:', error);
} finally {
await db.sequelize.close();
}
}
checkAreaManager();

View File

@ -1,34 +0,0 @@
import { Sequelize } from 'sequelize';
const sequelize = new Sequelize('royal_enfield_onboarding', 'laxman', '<.efvP1D0^80Z)r5', {
host: 'localhost',
dialect: 'postgres',
logging: false
});
const run = async () => {
try {
await sequelize.authenticate();
console.log('Connected to database.');
const [results] = await sequelize.query(`
SELECT column_name, data_type
FROM information_schema.columns
WHERE table_name = 'interview_evaluations';
`);
console.log('Columns in interview_evaluations:');
console.table(results);
const [evals] = await sequelize.query('SELECT * FROM "interview_evaluations" ORDER BY "createdAt" DESC LIMIT 1;');
console.log('Latest evaluation:');
console.log(evals[0]);
process.exit(0);
} catch (error) {
console.error('Error:', error);
process.exit(1);
}
};
run();

View File

@ -1,20 +0,0 @@
import db from '../src/database/models/index.js';
async function check() {
try {
const roles = await (db as any).Role.findAll();
console.log('--- ROLES START ---');
console.log(JSON.stringify(roles.map((r: any) => ({
name: r.roleName,
code: r.roleCode,
id: r.id
})), null, 2));
console.log('--- ROLES END ---');
process.exit(0);
} catch (error) {
console.error('Error listing roles:', error);
process.exit(1);
}
}
check();

View File

@ -1,53 +0,0 @@
/**
* Script to delete a test relocation request by requestId
* Usage: npx tsx scripts/delete-test-relocation.ts REL-1775129490244-5B9C
*/
import db from '../src/database/models/index.js';
async function deleteRelocationRequest(requestId: string) {
try {
console.log(`Deleting relocation request: ${requestId}`);
// Find the request
const request = await db.RelocationRequest.findOne({
where: { requestId }
});
if (!request) {
console.log(`Request ${requestId} not found`);
process.exit(0);
}
// Delete associated RequestParticipants
await db.RequestParticipant.destroy({
where: { requestId: request.id, requestType: 'relocation' }
});
console.log('Deleted associated participants');
// Delete associated Worknotes
await db.Worknote.destroy({
where: { requestId: request.id, requestType: 'relocation' }
});
console.log('Deleted associated worknotes');
// Delete the request
await request.destroy();
console.log(`Deleted relocation request: ${requestId}`);
console.log('✅ Done!');
process.exit(0);
} catch (error) {
console.error('❌ Error:', error);
process.exit(1);
} finally {
await db.sequelize.close();
}
}
const requestId = process.argv[2];
if (!requestId) {
console.log('Usage: npx tsx scripts/delete-test-relocation.ts <requestId>');
process.exit(1);
}
deleteRelocationRequest(requestId);

View File

@ -1,76 +0,0 @@
import dotenv from 'dotenv';
import path from 'path';
import { fileURLToPath } from 'url';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
dotenv.config({ path: path.join(__dirname, '../.env') });
import db from '../src/database/models/index.js';
async function run() {
try {
console.log('Associations for User:');
const userAssoc = db.User.associations;
Object.keys(userAssoc).forEach(key => {
console.log(`- ${key}: ${userAssoc[key].associationType} to ${userAssoc[key].target.name}`);
});
console.log('\nTrying findAll with managedAsmDistricts...');
await db.User.findAll({
limit: 1,
include: [{ model: db.District, as: 'managedAsmDistricts' }]
});
console.log('Success with managedAsmDistricts');
console.log('\nTrying findAll with managedAreaDistricts...');
await db.User.findAll({
limit: 1,
include: [{ model: db.District, as: 'managedAreaDistricts' }]
});
console.log('Success with managedAreaDistricts');
console.log('\nTrying FULL query from getASMs...');
await db.User.findAll({
where: {
roleCode: { [db.Sequelize.Op.in]: ['ASM', 'AREA SALES MANAGER', 'DD-AM', 'DD AM'] },
isActive: true
},
include: [
{
model: db.UserRole,
as: 'userRoles',
where: { isActive: true },
required: false,
include: [{ model: db.Role, as: 'role', where: { roleCode: { [db.Sequelize.Op.in]: ['ASM', 'DD-AM', 'DD AM'] } } }]
},
{
model: db.District,
as: 'managedAsmDistricts',
include: [
{ model: db.State, as: 'state', attributes: ['id', 'name'] },
{ model: db.Region, as: 'region', attributes: ['id', 'name'] },
{ model: db.Zone, as: 'zone', attributes: ['id', 'name'] }
]
},
{
model: db.District,
as: 'managedAreaDistricts',
include: [
{ model: db.State, as: 'state', attributes: ['id', 'name'] },
{ model: db.Region, as: 'region', attributes: ['id', 'name'] },
{ model: db.Zone, as: 'zone', attributes: ['id', 'name'] }
]
}
],
});
console.log('Success with FULL query');
process.exit(0);
} catch (error) {
console.error('Error:', error);
process.exit(1);
}
}
run();

View File

@ -1,24 +0,0 @@
import dotenv from 'dotenv';
import path from 'path';
import { fileURLToPath } from 'url';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
dotenv.config({ path: path.join(__dirname, '../.env') });
import db from '../src/database/models/index.js';
async function run() {
try {
const u = await db.User.findOne({
where: { fullName: { [db.Sequelize.Op.iLike]: '%abhishek%' } }
});
console.log(JSON.stringify(u, null, 2));
process.exit(0);
} catch (error) {
console.error('Error:', error);
process.exit(1);
}
}
run();

View File

@ -1,26 +0,0 @@
import { Sequelize } from 'sequelize';
const sequelize = new Sequelize('royal_enfield_onboarding', 'laxman', 'Admin@123', {
host: 'localhost',
dialect: 'postgres',
logging: console.log
});
const run = async () => {
try {
await sequelize.authenticate();
console.log('Connected to database.');
console.log('Adding asmCode column to area_managers table...');
await sequelize.query('ALTER TABLE "area_managers" ADD COLUMN IF NOT EXISTS "asmCode" VARCHAR(255);');
console.log('Column added successfully.');
process.exit(0);
} catch (error) {
console.error('Error:', error);
process.exit(1);
}
};
run();

View File

@ -1,26 +0,0 @@
import { Sequelize } from 'sequelize';
const sequelize = new Sequelize('royal_enfield_onboarding', 'laxman', '<.efvP1D0^80Z)r5', {
host: 'localhost',
dialect: 'postgres',
logging: console.log
});
const run = async () => {
try {
await sequelize.authenticate();
console.log('Connected to database.');
console.log('Adding remarks column to interview_evaluations table...');
await sequelize.query('ALTER TABLE "interview_evaluations" ADD COLUMN IF NOT EXISTS "remarks" TEXT;');
console.log('Column added successfully.');
process.exit(0);
} catch (error) {
console.error('Error:', error);
process.exit(1);
}
};
run();

View File

@ -1,47 +0,0 @@
import 'dotenv/config';
import { Sequelize } from 'sequelize';
const sequelize = new Sequelize('royal_enfield_onboarding', 'laxman', '<.efvP1D0^80Z)r5', {
host: 'localhost',
dialect: 'postgres',
logging: console.log
});
const run = async () => {
try {
await sequelize.authenticate();
console.log('Connected to database.');
const stagesToAdd = [
'Level 1 Approved',
'Level 2 Approved',
'Level 2 Recommended',
'Level 3 Approved'
];
for (const stage of stagesToAdd) {
try {
await sequelize.query(`ALTER TYPE "enum_applications_currentStage" ADD VALUE IF NOT EXISTS '${stage}';`);
console.log(`Added '${stage}' to enum_applications_currentStage`);
} catch (e: any) {
console.log(`'${stage}' might already exist in enum_applications_currentStage or error:`, e.message);
}
try {
await sequelize.query(`ALTER TYPE "enum_applications_overallStatus" ADD VALUE IF NOT EXISTS '${stage}';`);
console.log(`Added '${stage}' to enum_applications_overallStatus`);
} catch (e: any) {
console.log(`'${stage}' might already exist in enum_applications_overallStatus or error:`, e.message);
}
}
console.log('Successfully updated enums.');
process.exit(0);
} catch (error) {
console.error('Error:', error);
process.exit(1);
}
};
run();

View File

@ -1,40 +0,0 @@
import dotenv from 'dotenv';
import path from 'path';
import { fileURLToPath } from 'url';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
dotenv.config({ path: path.join(__dirname, '../.env') });
import db from '../src/database/models/index.js';
async function run() {
try {
const district = await db.District.findOne({
where: { name: { [db.Sequelize.Op.iLike]: '%South Delhi%' } }
});
if (!district) {
console.log('South Delhi not found');
return;
}
console.log('Current assignment for South Delhi:');
console.log(`ddAmId: ${district.ddAmId}`);
console.log(`asmId: ${district.asmId}`);
console.log(`zmId: ${district.zmId}`);
if (district.asmId) {
console.log(`Removing ASM ${district.asmId} from South Delhi...`);
await district.update({ asmId: null, asmCode: null });
console.log('ASM removed.');
}
process.exit(0);
} catch (error) {
console.error('Error:', error);
process.exit(1);
}
}
run();

View File

@ -1,21 +0,0 @@
import db from '../src/database/models/index.ts';
const syncDb = async () => {
try {
console.log('Connecting to database...');
await db.sequelize.authenticate();
console.log('Database connected.');
console.log('Syncing database schema (alter: true)...');
await db.sequelize.sync({ alter: true });
console.log('Database synced successfully.');
process.exit(0);
} catch (error) {
console.error('Error syncing database:', error);
process.exit(1);
}
};
syncDb();

View File

@ -1,29 +0,0 @@
import { Sequelize } from 'sequelize';
const sequelize = new Sequelize('royal_enfield_onboarding', 'laxman', '<.efvP1D0^80Z)r5', {
host: 'localhost',
dialect: 'postgres',
logging: console.log
});
const run = async () => {
try {
await sequelize.authenticate();
console.log('Connected to database.');
console.log('Renaming recommendation to decision and remarks to decisionRemarks in interview_evaluations...');
// Use a transaction for safety
await sequelize.query('ALTER TABLE "interview_evaluations" RENAME COLUMN "recommendation" TO "decision";');
await sequelize.query('ALTER TABLE "interview_evaluations" RENAME COLUMN "remarks" TO "decisionRemarks";');
console.log('Columns renamed successfully.');
process.exit(0);
} catch (error) {
console.error('Error during migration:', error);
process.exit(1);
}
};
run();

View File

@ -1,83 +0,0 @@
/**
* Migration Script: Clean up onboarding_documents table.
*
* What it does (idempotent safe to re-run):
* 1. Drops legacy columns `requestId` and `requestType` (and their indexes).
* These were generic catch-alls from when a single documents table routed
* across modules. Each module now has its own dedicated documents table
* (resignation_documents, termination_documents, constitutional_documents,
* relocation_documents), so these columns are dead weight on
* onboarding_documents and are not read or written anywhere in code.
* 2. Adds two indexes the UI actually queries:
* - (applicationId, stage) -> Progress / Documents tab grouping
* - documentType -> EOR auto-link in onboarding.controller.ts
*
* What it does NOT do:
* - No new "documentName" column. The user-entered document name is sent as
* the FormData filename and stored in the existing `fileName` column.
* - Does not touch `dealerId` (the Dealer <-> OnboardingDocument association
* references it; it stays for future use).
*
* Run: npx tsx scripts/migrate-onboarding-documents-cleanup.ts
*/
import 'dotenv/config';
import db from '../src/database/models/index.js';
const TABLE = 'onboarding_documents';
async function migrate() {
const queryInterface = db.sequelize.getQueryInterface();
try {
console.log(`🔄 Cleaning up ${TABLE} ...\n`);
await db.sequelize.authenticate();
const tableInfo = await queryInterface.describeTable(TABLE);
// 1) Drop the index on requestId first (if it exists). Index name depends on
// how Sequelize/Postgres generated it — try the common variants.
for (const idxName of [
`${TABLE}_requestId`,
`${TABLE}_request_id`,
]) {
try {
await db.sequelize.query(`DROP INDEX IF EXISTS "${idxName}"`);
console.log(`✓ Dropped index ${idxName} (if existed)`);
} catch (err: any) {
console.log(`- Skipped index ${idxName}: ${err.message}`);
}
}
// 2) Drop the unused columns (idempotent via describeTable check).
for (const col of ['requestId', 'requestType']) {
if (tableInfo[col]) {
console.log(`Dropping column ${col} ...`);
await queryInterface.removeColumn(TABLE, col);
console.log(`✓ Dropped column ${col}`);
} else {
console.log(`- Column ${col} not present (already cleaned)`);
}
}
// 3) Add useful indexes (idempotent — Postgres IF NOT EXISTS).
await db.sequelize.query(
`CREATE INDEX IF NOT EXISTS "${TABLE}_applicationId_stage" ON ${TABLE} ("applicationId", "stage")`
);
console.log(`✓ Ensured index ${TABLE}_applicationId_stage`);
await db.sequelize.query(
`CREATE INDEX IF NOT EXISTS "${TABLE}_documentType" ON ${TABLE} ("documentType")`
);
console.log(`✓ Ensured index ${TABLE}_documentType`);
console.log('\n✅ Migration completed successfully!');
} catch (error: any) {
console.error('\n❌ Migration failed:', error.message);
if (error.stack) console.error('\nStack Trace:\n', error.stack);
process.exit(1);
} finally {
await db.sequelize.close();
}
}
migrate();

View File

@ -1,74 +0,0 @@
/**
* Migration Script: Add newDistrictId and newStateId to RelocationRequest
* Run: npx ts-node scripts/migrate-relocation-schema.ts
*/
import db from '../src/database/models/index.js';
async function migrate() {
const queryInterface = db.sequelize.getQueryInterface();
try {
console.log('Starting relocation schema migration...');
// Get table description to check existing columns
const tableInfo = await queryInterface.describeTable('relocation_requests');
// Add newDistrictId column if not exists
if (!tableInfo.newDistrictId) {
console.log('Adding newDistrictId column...');
await queryInterface.addColumn('relocation_requests', 'newDistrictId', {
type: db.Sequelize.DataTypes.UUID,
allowNull: true,
references: {
model: 'districts',
key: 'id'
}
});
console.log('✓ newDistrictId column added');
} else {
console.log('- newDistrictId column already exists');
}
// Add newStateId column if not exists
if (!tableInfo.newStateId) {
console.log('Adding newStateId column...');
await queryInterface.addColumn('relocation_requests', 'newStateId', {
type: db.Sequelize.DataTypes.UUID,
allowNull: true,
references: {
model: 'states',
key: 'id'
}
});
console.log('✓ newStateId column added');
} else {
console.log('- newStateId column already exists');
}
// Update enum to include 'Intercity' if not already present
console.log('Checking relocationType enum...');
try {
await db.sequelize.query(`
ALTER TYPE "enum_relocation_requests_relocationType"
ADD VALUE IF NOT EXISTS 'Intercity';
`);
console.log('✓ Intercity added to enum (if not already present)');
} catch (enumError: any) {
// PostgreSQL doesn't support IF NOT EXISTS for enum values in some versions
if (enumError.code === '42710') {
console.log('- Intercity already exists in enum');
} else {
console.log('Warning: Could not update enum:', enumError.message);
}
}
console.log('\n✅ Migration completed successfully!');
} catch (error) {
console.error('❌ Migration failed:', error);
process.exit(1);
} finally {
await db.sequelize.close();
}
}
migrate();

View File

@ -1,49 +0,0 @@
import 'dotenv/config';
import db from '../src/database/models/index.js';
/**
* Creates sla_notification_dispatches idempotent audit log for SLA emails/alerts.
* Safe to run multiple times.
*/
async function migrate() {
const { sequelize } = db as { sequelize: { authenticate: () => Promise<void>; query: (sql: string) => Promise<unknown>; close: () => Promise<void> } };
await sequelize.authenticate();
console.log('Database connected.');
const statements = [
`CREATE TABLE IF NOT EXISTS sla_notification_dispatches (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
"trackingId" UUID NOT NULL REFERENCES sla_tracking(id) ON DELETE CASCADE,
"thresholdKey" VARCHAR(128) NOT NULL,
"dispatchType" VARCHAR(32) NOT NULL,
"templateCode" VARCHAR(64),
"stageName" VARCHAR(255),
"reminderId" UUID,
"escalationLevel" INTEGER,
"recipientCount" INTEGER NOT NULL DEFAULT 0,
"sentAt" TIMESTAMPTZ NOT NULL DEFAULT NOW(),
status VARCHAR(24) NOT NULL DEFAULT 'sent',
"createdAt" TIMESTAMPTZ NOT NULL DEFAULT NOW(),
"updatedAt" TIMESTAMPTZ NOT NULL DEFAULT NOW()
)`,
`CREATE UNIQUE INDEX IF NOT EXISTS sla_notification_dispatches_tracking_threshold_uq
ON sla_notification_dispatches ("trackingId", "thresholdKey")`,
`CREATE INDEX IF NOT EXISTS sla_notification_dispatches_tracking_sent_idx
ON sla_notification_dispatches ("trackingId", "sentAt")`,
`CREATE INDEX IF NOT EXISTS sla_notification_dispatches_type_idx
ON sla_notification_dispatches ("dispatchType")`
];
for (const sql of statements) {
console.log('Running:', sql.split('\n')[0].slice(0, 72) + '...');
await sequelize.query(sql);
}
console.log('sla_notification_dispatches migration complete.');
await sequelize.close();
}
migrate().catch((err) => {
console.error(err);
process.exit(1);
});

View File

@ -1,34 +0,0 @@
import 'dotenv/config';
import db from '../src/database/models/index.js';
/**
* Aligns sla_tracking with SLATracking model (entity columns + metadata for reminder state).
* Safe to run multiple times (IF NOT EXISTS).
*/
async function migrate() {
const { sequelize } = db as any;
await sequelize.authenticate();
console.log('Database connected.');
const statements = [
`ALTER TABLE sla_tracking ADD COLUMN IF NOT EXISTS "entityType" VARCHAR(255)`,
`ALTER TABLE sla_tracking ADD COLUMN IF NOT EXISTS "entityId" UUID`,
`ALTER TABLE sla_tracking ADD COLUMN IF NOT EXISTS metadata JSONB DEFAULT '{}'::jsonb`,
// Backfill entity columns for legacy rows that only had applicationId
`UPDATE sla_tracking SET "entityType" = 'application' WHERE "entityType" IS NULL AND "applicationId" IS NOT NULL`,
`UPDATE sla_tracking SET "entityId" = "applicationId" WHERE "entityId" IS NULL AND "applicationId" IS NOT NULL`
];
for (const sql of statements) {
console.log('Running:', sql.slice(0, 80) + '...');
await sequelize.query(sql);
}
console.log('sla_tracking schema migration complete.');
await sequelize.close();
}
migrate().catch((err) => {
console.error(err);
process.exit(1);
});

View File

@ -1,36 +1,89 @@
/** /**
* Database Migration Script * Database Migration Script destructive fresh sync.
* Synchronizes all Sequelize models with the database (PostgreSQL).
* This script will DROP all existing tables and recreate them.
* *
* Schema for modules such as constitutional change (ENUM values, partial unique indexes, * Drops every table and recreates the schema from Sequelize model definitions
* columns) is defined only on Sequelize models no separate "table alteration" scripts are * in `src/database/models/`. After the fresh schema is in place, every
* required after a fresh `migrate` + `seed:all` (see package.json `setup:fresh`). * versioned migration file under `scripts/migrations/` is automatically
* stamped into the `migrations` table as "already applied" so subsequent
* `npm run migrate:up` runs on this DB will be no-ops until a newer
* migration is added.
*
* For incremental schema changes on environments that already hold data,
* use `npm run migrate:up` instead.
* *
* Run: npx tsx scripts/migrate.ts * Run: npx tsx scripts/migrate.ts
* Flags:
* --no-baseline Skip stamping migration files as applied (advanced).
*/ */
import 'dotenv/config'; import 'dotenv/config';
import { promises as fs } from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
import { createHash } from 'crypto';
import db from '../src/database/models/index.js'; import db from '../src/database/models/index.js';
const MIGRATIONS_DIR = path.resolve(
path.dirname(fileURLToPath(import.meta.url)),
'migrations'
);
async function discoverMigrations(): Promise<string[]> {
try {
const entries = await fs.readdir(MIGRATIONS_DIR);
return entries
.filter((name) => name.endsWith('.ts') && !name.startsWith('_'))
.sort();
} catch {
return [];
}
}
async function fileChecksum(file: string): Promise<string> {
const buf = await fs.readFile(path.join(MIGRATIONS_DIR, file));
return createHash('sha256').update(buf).digest('hex');
}
async function baselineMigrationsTable(): Promise<void> {
const files = await discoverMigrations();
if (files.length === 0) {
console.log('No versioned migrations to baseline.');
return;
}
console.log(`📌 Stamping ${files.length} migration(s) as already-applied:`);
for (const file of files) {
const name = file.replace(/\.ts$/, '');
const checksum = await fileChecksum(file);
await db.Migration.create({ name, checksum });
console.log(` + ${name}`);
}
}
async function runMigrations() { async function runMigrations() {
console.log('🔄 Starting database synchronization (Fresh Startup)...\n'); console.log('🔄 Starting database synchronization (Fresh Startup)...\n');
console.log('⚠️ WARNING: This will drop all existing tables in the database.\n'); console.log('⚠️ WARNING: This will drop all existing tables in the database.\n');
const skipBaseline = process.argv.includes('--no-baseline');
try { try {
// Authenticate with the database
await db.sequelize.authenticate(); await db.sequelize.authenticate();
console.log('📡 Connected to the database successfully.'); console.log('📡 Connected to the database successfully.');
// Synchronize models (force: true drops existing tables) // force: true drops existing tables — schema is rebuilt exactly from
// This ensures that the schema exactly matches the Sequelize models // Sequelize models, so every enum / column / index matches code.
await db.sequelize.sync({ force: true }); await db.sequelize.sync({ force: true });
console.log('\n✅ All tables created and synchronized successfully!'); console.log('\n✅ All tables created and synchronized successfully!');
console.log('----------------------------------------------------'); console.log('----------------------------------------------------');
const modelNames = Object.keys(db).filter(k => k !== 'sequelize' && k !== 'Sequelize'); const modelNames = Object.keys(db).filter((k) => k !== 'sequelize' && k !== 'Sequelize');
console.log(`Available Models (${modelNames.length}): ${modelNames.join(', ')}`); console.log(`Available Models (${modelNames.length}): ${modelNames.join(', ')}`);
console.log('----------------------------------------------------'); console.log('----------------------------------------------------\n');
if (!skipBaseline) {
await baselineMigrationsTable();
} else {
console.log('Skipping migration baseline (--no-baseline).');
}
process.exit(0); process.exit(0);
} catch (error: any) { } catch (error: any) {

View File

@ -1,63 +0,0 @@
import { Sequelize } from 'sequelize';
import config from '../src/common/config/database.js';
const env = process.env.NODE_ENV || 'development';
const dbConfig = config[env];
const sequelize = new Sequelize(
dbConfig.database,
dbConfig.username,
dbConfig.password,
{
host: dbConfig.host,
port: dbConfig.port,
dialect: dbConfig.dialect,
logging: console.log
}
);
async function migrate() {
try {
await sequelize.authenticate();
console.log('Connected to database.');
const queryInterface = sequelize.getQueryInterface();
// Check if users table exists
const tables = await queryInterface.showAllTables();
if (!tables.includes('users')) {
console.log('Users table does not exist. Skipping rename.');
return;
}
// Rename fullName to name
const columns = await queryInterface.describeTable('users');
if (columns.fullName && !columns.name) {
console.log('Renaming fullName to name...');
await queryInterface.renameColumn('users', 'fullName', 'name');
} else if (columns.fullName && columns.name) {
console.log('Both fullName and name exist. Manual intervention needed.');
} else {
console.log('fullName not found or name already exists.');
}
// Rename mobileNumber to phone
if (columns.mobileNumber && !columns.phone) {
console.log('Renaming mobileNumber to phone...');
await queryInterface.renameColumn('users', 'mobileNumber', 'phone');
} else if (columns.mobileNumber && columns.phone) {
console.log('Both mobileNumber and phone exist. Manual intervention needed.');
} else {
console.log('mobileNumber not found or phone already exists.');
}
console.log('Migration successful.');
} catch (error) {
console.error('Migration failed:', error);
} finally {
await sequelize.close();
}
}
migrate();

View File

@ -0,0 +1,52 @@
/**
* Migration: create sla_notification_dispatches
*
* Folds the legacy `scripts/migrate-sla-notification-dispatches.ts` script
* into the new versioned migrations system. Creates the idempotent dispatch
* audit log used to dedupe SLA emails/alerts per tracking entry + threshold.
*
* Fresh `npm run migrate` runs already build this table from
* `SLANotificationDispatch` model this migration exists so environments
* that predate the model can catch up via `npm run migrate:up`.
*/
import type { Sequelize, Transaction } from 'sequelize';
export interface MigrationContext {
sequelize: Sequelize;
transaction: Transaction;
}
const migration = {
async up({ sequelize, transaction }: MigrationContext): Promise<void> {
const statements = [
`CREATE TABLE IF NOT EXISTS sla_notification_dispatches (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
"trackingId" UUID NOT NULL REFERENCES sla_tracking(id) ON DELETE CASCADE,
"thresholdKey" VARCHAR(128) NOT NULL,
"dispatchType" VARCHAR(32) NOT NULL,
"templateCode" VARCHAR(64),
"stageName" VARCHAR(255),
"reminderId" UUID,
"escalationLevel" INTEGER,
"recipientCount" INTEGER NOT NULL DEFAULT 0,
"sentAt" TIMESTAMPTZ NOT NULL DEFAULT NOW(),
status VARCHAR(24) NOT NULL DEFAULT 'sent',
"createdAt" TIMESTAMPTZ NOT NULL DEFAULT NOW(),
"updatedAt" TIMESTAMPTZ NOT NULL DEFAULT NOW()
)`,
`CREATE UNIQUE INDEX IF NOT EXISTS sla_notification_dispatches_tracking_threshold_uq
ON sla_notification_dispatches ("trackingId", "thresholdKey")`,
`CREATE INDEX IF NOT EXISTS sla_notification_dispatches_tracking_sent_idx
ON sla_notification_dispatches ("trackingId", "sentAt")`,
`CREATE INDEX IF NOT EXISTS sla_notification_dispatches_type_idx
ON sla_notification_dispatches ("dispatchType")`
];
for (const sql of statements) {
await sequelize.query(sql, { transaction });
}
}
};
export default migration;

View File

@ -0,0 +1,58 @@
# Database Migrations
This folder holds versioned, incremental database migrations for environments
that already have a populated schema (UAT / production). On a fresh dev box,
`npm run migrate` (destructive `sync({ force: true })`) is still the fastest
route — the model definitions in `src/database/models/` remain the source of
truth for the desired schema.
## Workflow
| Goal | Command |
|-----------------------------------------------------|---------|
| Fresh / dev: drop everything, recreate from models | `npm run migrate` |
| Mark every migration file here as already-applied | `npm run migrate:baseline` |
| Apply only the migrations not yet recorded in DB | `npm run migrate:up` |
| List applied vs pending | `npm run migrate:status` |
| Scaffold a new migration file | `npm run migrate:create -- <snake_case_name>` |
A typical post-fresh-setup sequence is therefore:
```bash
npm run migrate # drop + recreate
npm run migrate:baseline # stamp this folder's files as already applied
npm run seed:all # seed master data
```
After every subsequent deploy on the same DB:
```bash
npm run migrate:up # apply only the new migrations
```
## File naming convention
```
<YYYYMMDDHHMMSS>_<snake_case_description>.ts
```
Examples:
- `20260526143000_add_finance_kyc_column.ts`
- `20260601090000_drop_legacy_questionnaire_column.ts`
The runner sorts files lexicographically, so the timestamp prefix dictates
execution order. Always use UTC when manually creating timestamps; the
scaffolder (`npm run migrate:create`) emits a current-time UTC stamp for you.
## Authoring a migration
1. Run `npm run migrate:create -- add_finance_kyc_column`.
2. Edit the generated file — implement `up({ sequelize })`.
3. Update the corresponding Sequelize model so fresh `migrate` runs produce
the same end state.
4. Commit both the migration and the model changes in the same PR.
5. On every environment that holds real data, run `npm run migrate:up`.
The runner records each successful migration in the `migrations` table
(`{ name, appliedAt, checksum }`) so re-runs are safe and idempotent at the
runner level — independently of the migration's own SQL.

View File

@ -0,0 +1,42 @@
/**
* Migration template copy into a new file via `npm run migrate:create -- <name>`.
*
* File naming convention: `<YYYYMMDDHHMMSS>_<snake_case_description>.ts`
* e.g. `20260526143000_add_finance_kyc_column.ts`
*
* The runner (`scripts/run-migrations.ts`) imports the default export and
* invokes `up()`. After `up()` resolves it records the filename (without
* extension) in the `migrations` table so the migration is never re-run on
* this environment.
*
* Guidelines:
* - Always wrap multi-statement changes in a single Sequelize transaction.
* - Prefer idempotent DDL (`IF NOT EXISTS`, `IF EXISTS`) so accidental
* re-runs are safe.
* - Never destructively drop columns/tables that hold real production data
* unless you have a separate, explicit data-migration step.
* - Update the corresponding Sequelize model in `src/database/models/`
* in the same PR migrations are a delta for environments that already
* have a populated schema; the model definitions remain the source of
* truth for fresh `npm run migrate` builds.
*/
import type { QueryInterface, Sequelize } from 'sequelize';
export interface MigrationContext {
queryInterface: QueryInterface;
sequelize: Sequelize;
}
const migration = {
async up({ sequelize }: MigrationContext): Promise<void> {
// Example:
// await sequelize.query(`
// ALTER TABLE "applications"
// ADD COLUMN IF NOT EXISTS "kycReviewedAt" TIMESTAMPTZ NULL;
// `);
throw new Error('Migration template — implement up() before running.');
}
};
export default migration;

View File

@ -1,36 +0,0 @@
import dotenv from 'dotenv';
import path from 'path';
import { fileURLToPath } from 'url';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
dotenv.config({ path: path.join(__dirname, '../.env') });
import db from '../src/database/models/index.js';
async function run() {
try {
const appId = '1f1fec7d-7034-4588-a4b2-0e1d4cc3f149';
const abhishekId = '9284a190-f4d2-49f3-9186-bb7c93dc9b6d';
const deleted = await db.RequestParticipant.destroy({
where: {
requestId: appId,
userId: abhishekId
}
});
if (deleted) {
console.log('Successfully removed Abhishek from application participants.');
} else {
console.log('Abhishek was not found in participants for this application.');
}
process.exit(0);
} catch (error) {
console.error('Error:', error);
process.exit(1);
}
}
run();

181
scripts/run-migrations.ts Normal file
View File

@ -0,0 +1,181 @@
/**
* Migration runner.
*
* Usage:
* npm run migrate:up -> apply pending migrations
* npm run migrate:status -> list applied vs pending
* npm run migrate:baseline -> mark every existing migration as already applied
*
* The runner is intentionally minimal: it discovers `.ts` files inside
* `scripts/migrations/`, ignores anything starting with `_` (template files,
* shared helpers), sorts them lexicographically (filename-based ordering ==
* timestamp ordering when authors follow the convention), and applies each
* file whose `name` is not yet recorded in the `migrations` table.
*
* Each migration file must `export default` an object exposing
* async up({ sequelize, queryInterface }): Promise<void>
* See `scripts/migrations/_template.ts` for the contract.
*/
import 'dotenv/config';
import { fileURLToPath, pathToFileURL } from 'url';
import { createHash } from 'crypto';
import { promises as fs } from 'fs';
import path from 'path';
import db from '../src/database/models/index.js';
const MIGRATIONS_DIR = path.resolve(
path.dirname(fileURLToPath(import.meta.url)),
'migrations'
);
type Mode = 'up' | 'status' | 'baseline';
function parseMode(argv: string[]): Mode {
if (argv.includes('--status')) return 'status';
if (argv.includes('--baseline')) return 'baseline';
return 'up';
}
async function discoverMigrationFiles(): Promise<string[]> {
const entries = await fs.readdir(MIGRATIONS_DIR);
return entries
.filter((name) => name.endsWith('.ts') && !name.startsWith('_'))
.sort();
}
async function ensureMigrationsTable(): Promise<void> {
// The Migration model has the canonical schema. We create the table only
// if it does not yet exist so this script remains safe to run on any
// environment (fresh sync OR already-migrated DB).
await db.Migration.sync();
}
async function loadMigration(file: string): Promise<{ up: (ctx: any) => Promise<void> }> {
const fullPath = path.join(MIGRATIONS_DIR, file);
const mod: any = await import(pathToFileURL(fullPath).href);
const migration = mod.default ?? mod;
if (!migration || typeof migration.up !== 'function') {
throw new Error(`Migration ${file} does not export a default { up } function.`);
}
return migration;
}
async function fileChecksum(file: string): Promise<string> {
const buf = await fs.readFile(path.join(MIGRATIONS_DIR, file));
return createHash('sha256').update(buf).digest('hex');
}
async function listApplied(): Promise<Set<string>> {
const rows = await db.Migration.findAll({ attributes: ['name'] });
return new Set(rows.map((r: any) => r.name as string));
}
function stripExt(file: string): string {
return file.replace(/\.ts$/, '');
}
async function runUp(): Promise<void> {
const files = await discoverMigrationFiles();
const applied = await listApplied();
const pending = files.filter((f) => !applied.has(stripExt(f)));
if (pending.length === 0) {
console.log('All migrations are already applied. Nothing to do.');
return;
}
console.log(`Applying ${pending.length} migration(s)...\n`);
for (const file of pending) {
const name = stripExt(file);
process.stdout.write(`${name} ... `);
try {
const migration = await loadMigration(file);
await db.sequelize.transaction(async (transaction: any) => {
await migration.up({
sequelize: db.sequelize,
queryInterface: db.sequelize.getQueryInterface(),
transaction
});
});
const checksum = await fileChecksum(file);
await db.Migration.create({ name, checksum });
console.log('OK');
} catch (err: any) {
console.log('FAILED');
console.error(`\n${file} failed:`, err.message || err);
throw err;
}
}
console.log('\nMigrations complete.');
}
async function runStatus(): Promise<void> {
const files = await discoverMigrationFiles();
const applied = await listApplied();
if (files.length === 0) {
console.log('No migration files in scripts/migrations/.');
return;
}
console.log('Migration status:');
for (const file of files) {
const name = stripExt(file);
const mark = applied.has(name) ? '✓' : ' ';
console.log(` [${mark}] ${name}`);
}
const pendingCount = files.filter((f) => !applied.has(stripExt(f))).length;
console.log(`\nApplied: ${files.length - pendingCount} Pending: ${pendingCount}`);
}
async function runBaseline(): Promise<void> {
const files = await discoverMigrationFiles();
const applied = await listApplied();
const toStamp = files.filter((f) => !applied.has(stripExt(f)));
if (toStamp.length === 0) {
console.log('Baseline: every migration is already recorded. Nothing to do.');
return;
}
console.log(`Stamping ${toStamp.length} migration(s) as already-applied (no SQL executed):\n`);
for (const file of toStamp) {
const name = stripExt(file);
const checksum = await fileChecksum(file);
await db.Migration.create({ name, checksum });
console.log(` + ${name}`);
}
console.log('\nBaseline complete.');
}
async function main(): Promise<void> {
const mode = parseMode(process.argv.slice(2));
try {
await db.sequelize.authenticate();
await ensureMigrationsTable();
if (mode === 'status') {
await runStatus();
} else if (mode === 'baseline') {
await runBaseline();
} else {
await runUp();
}
await db.sequelize.close();
process.exit(0);
} catch (err: any) {
console.error('\nMigration runner aborted:', err?.message || err);
try {
await db.sequelize.close();
} catch {
/* ignore */
}
process.exit(1);
}
}
main();

View File

@ -1,21 +0,0 @@
import db from '../src/database/models/index.js';
const { Area, District, User } = db;
async function testAreas() {
try {
console.log('Testing Area.findAll...');
const areas = await Area.findAll({
include: [
{ model: District, as: 'district', attributes: ['districtName'] },
{ model: User, as: 'manager', attributes: ['id', 'fullName', 'email', 'mobileNumber'] }
],
order: [['areaName', 'ASC']]
});
console.log('Successfully fetched areas:', JSON.stringify(areas, null, 2));
} catch (error) {
console.error('Error fetching areas:', error);
}
}
testAreas();

View File

@ -1,34 +0,0 @@
import db from '../src/database/models/index.js';
const { Region, Zone, State, User } = db;
async function testRegions() {
try {
console.log('Testing Region.findAll...');
const regions = await Region.findAll({
include: [
{
model: State,
as: 'states',
attributes: ['id', 'stateName']
},
{
model: Zone,
as: 'zone',
attributes: ['id', 'zoneName']
},
{
model: User,
as: 'regionalManager',
attributes: ['id', 'fullName', 'email', 'mobileNumber']
}
],
order: [['regionName', 'ASC']]
});
console.log('Successfully fetched regions:', JSON.stringify(regions, null, 2));
} catch (error) {
console.error('Error fetching regions:', error);
}
}
testRegions();

View File

@ -1,26 +0,0 @@
import db from '../src/database/models/index.js';
async function testInsert() {
try {
// Attempt insert without checking existing records
// If it fails with "invalid input value", the enum is truly not updated.
// If it fails with "foreign key", the enum was VALID but the data was wrong.
await db.sequelize.query(`
DO $$
BEGIN
-- This will fail if 'architecture' is invalid for the enum
PERFORM 'architecture'::"enum_request_participants_participantType";
RAISE NOTICE '✅ Enum check passed!';
EXCEPTION WHEN OTHERS THEN
RAISE EXCEPTION '❌ Enum check failed: %', SQLERRM;
END $$;
`);
console.log('✅ PL/pgSQL Enum check passed!');
} catch (error: any) {
console.error(error.message);
} finally {
process.exit(0);
}
}
testInsert();

View File

@ -1,22 +0,0 @@
import db from '../src/database/models/index.js';
async function testInsert() {
try {
const testId = '00000000-0000-0000-0000-000000000000';
await db.sequelize.query(`
INSERT INTO request_participants ("id", "requestId", "requestType", "userId", "participantType", "joinedMethod", "createdAt", "updatedAt")
VALUES (gen_random_uuid(), '${testId}', 'test', '9950ee60-ddf6-4091-a1e6-e7161e6d8bb6', 'architecture', 'manual', now(), now())
`);
console.log('✅ Manual insert successful!');
// Clean up
await db.sequelize.query(`DELETE FROM request_participants WHERE "requestId" = '${testId}'`);
console.log('✅ Clean up successful!');
} catch (error: any) {
console.error('❌ Manual insert failed:', error.message);
} finally {
process.exit(0);
}
}
testInsert();

View File

@ -1,42 +0,0 @@
import 'dotenv/config';
import db from '../src/database/models/index.js';
const updateEnum = async () => {
try {
console.log('>>> STARTING ENUM MIGRATION <<<');
await db.sequelize.authenticate();
console.log('Database connection established.');
// Raw query to add values to enum
// Note: PostgreSQL cannot remove enum values, only add.
// We will add the "Interview Pending" variations.
const queryInterface = db.sequelize.getQueryInterface();
try {
await db.sequelize.query(`ALTER TYPE "enum_applications_overallStatus" ADD VALUE IF NOT EXISTS 'Level 1 Interview Pending';`);
console.log('Added Level 1 Interview Pending');
} catch (e) { console.log('Level 1 Interview Pending likely exists or error', e.message); }
try {
await db.sequelize.query(`ALTER TYPE "enum_applications_overallStatus" ADD VALUE IF NOT EXISTS 'Level 2 Interview Pending';`);
console.log('Added Level 2 Interview Pending');
} catch (e) { console.log('Level 2 Interview Pending likely exists or error', e.message); }
try {
await db.sequelize.query(`ALTER TYPE "enum_applications_overallStatus" ADD VALUE IF NOT EXISTS 'Level 3 Interview Pending';`);
console.log('Added Level 3 Interview Pending');
} catch (e) { console.log('Level 3 Interview Pending likely exists or error', e.message); }
console.log('>>> SUCCESS: Enum values updated <<<');
await db.sequelize.close();
process.exit(0);
} catch (error) {
console.error('>>> ERROR: Failed to update Enum', error);
process.exit(1);
}
};
updateEnum();

View File

@ -1,46 +0,0 @@
import db from '../src/database/models/index.js';
const { sequelize } = db;
async function updateDealerCodesTable() {
console.log('🔄 Checking and updating dealer_codes table schema...');
try {
// Add applicationId
await sequelize.query(`
DO $$
BEGIN
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name='dealer_codes' AND column_name='applicationId') THEN
ALTER TABLE dealer_codes ADD COLUMN "applicationId" UUID REFERENCES applications(id);
END IF;
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name='dealer_codes' AND column_name='salesCode') THEN
ALTER TABLE dealer_codes ADD COLUMN "salesCode" VARCHAR(255);
END IF;
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name='dealer_codes' AND column_name='serviceCode') THEN
ALTER TABLE dealer_codes ADD COLUMN "serviceCode" VARCHAR(255);
END IF;
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name='dealer_codes' AND column_name='gmaCode') THEN
ALTER TABLE dealer_codes ADD COLUMN "gmaCode" VARCHAR(255);
END IF;
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name='dealer_codes' AND column_name='gearCode') THEN
ALTER TABLE dealer_codes ADD COLUMN "gearCode" VARCHAR(255);
END IF;
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name='dealer_codes' AND column_name='sapMasterId') THEN
ALTER TABLE dealer_codes ADD COLUMN "sapMasterId" VARCHAR(255);
END IF;
END $$;
`);
console.log('✅ dealer_codes table schema updated successfully.');
process.exit(0);
} catch (error) {
console.error('❌ Error updating dealer_codes table:', error);
process.exit(1);
}
}
updateDealerCodesTable();

View File

@ -1,33 +0,0 @@
import { APPLICATION_STATUS } from '../src/common/config/constants.js';
import db from '../src/database/models/index.js';
async function updateEnum() {
try {
console.log('🔄 Syncing all APPLICATION_STATUS values with DB Enum...');
const statuses = Object.values(APPLICATION_STATUS);
for (const status of statuses) {
try {
// Posgres doesn't support IF NOT EXISTS for ADD VALUE in 9.5 and below
// so we do it one by one and ignore "already exists" errors.
await db.sequelize.query(`ALTER TYPE "enum_applications_overallStatus" ADD VALUE '${status}'`);
console.log(`✅ Added: ${status}`);
} catch (e: any) {
if (e.message.includes('already exists')) {
// console.log(` Already exists: ${status}`);
} else {
console.error(`❌ Error adding ${status}:`, e.message);
}
}
}
console.log('\n✅ Database Enum successfully synchronized with constants.');
} catch (error: any) {
console.error('❌ Critical failure during sync:', error.message);
} finally {
process.exit(0);
}
}
updateEnum();

View File

@ -1,26 +0,0 @@
import db from '../src/database/models/index.js';
async function updateParticipantEnum() {
try {
console.log('🔄 Adding "architecture" to participantType enum...');
try {
await db.sequelize.query(`ALTER TYPE "enum_request_participants_participantType" ADD VALUE 'architecture'`);
console.log(`✅ Added: architecture`);
} catch (e: any) {
if (e.message.includes('already exists')) {
console.log(` Already exists: architecture`);
} else {
console.error(`❌ Error adding architecture:`, e.message);
}
}
console.log('\n✅ Database Enum successfully updated.');
} catch (error: any) {
console.error('❌ Critical failure:', error.message);
} finally {
process.exit(0);
}
}
updateParticipantEnum();

View File

@ -1,47 +0,0 @@
import assert from 'node:assert/strict';
import {
getResignationStatusForStage,
getTerminationStatusForStage,
normalizeClearanceStatus,
normalizeFnFStatus,
normalizeTerminationCurrentStage,
getLegacyTerminationRowFixes
} from '../src/common/utils/offboardingStatus.js';
import { getJointRoundCutoffMsFromTimeline } from '../src/common/utils/terminationJointReviewRound.util.js';
assert.equal(normalizeFnFStatus('settled'), 'Completed');
assert.equal(normalizeFnFStatus('finance approval'), 'Finance Approval');
assert.equal(getResignationStatusForStage('ASM'), 'ASM Review');
assert.equal(getResignationStatusForStage('F&F Initiated'), 'F&F Initiated');
assert.equal(getTerminationStatusForStage('Submitted'), 'Submitted');
assert.equal(getTerminationStatusForStage('Terminated'), 'Terminated');
assert.equal(
normalizeTerminationCurrentStage('Personal Hearing'),
'Evaluation of Dealer SCN Response'
);
assert.deepEqual(getLegacyTerminationRowFixes({ currentStage: 'Personal Hearing', status: 'Personal Hearing Pending' }), {
currentStage: 'Evaluation of Dealer SCN Response',
status: 'SCN Response Evaluation Pending'
});
const reconsiderTimeline = [
{ action: 'Approved', targetStage: 'NBH Final Approval', timestamp: new Date('2024-01-01').toISOString() },
{
action: 'Sent for Reconsideration',
targetStage: 'Evaluation of Dealer SCN Response',
timestamp: new Date('2025-06-15T12:00:00.000Z').toISOString()
}
];
assert.equal(
getJointRoundCutoffMsFromTimeline(reconsiderTimeline, 'scn_response_eval'),
new Date('2025-06-15T12:00:00.000Z').getTime()
);
assert.equal(normalizeClearanceStatus('Cleared', 0), 'NOC Submitted');
assert.equal(normalizeClearanceStatus('Cleared', 100), 'Dues Pending');
assert.equal(normalizeClearanceStatus('Pending', 0), 'Pending');
console.log('Offboarding status normalization checks passed.');

View File

@ -1,58 +0,0 @@
import assert from 'node:assert/strict';
import {
validateOffboardingAction,
getPreviousStage,
getOffboardingAuditAction
} from '../src/common/utils/offboardingWorkflow.utils.js';
import {
OFFBOARDING_ACTIONS,
REQUEST_TYPES,
TERMINATION_STAGES,
RESIGNATION_STAGES,
CONSTITUTIONAL_STAGES,
AUDIT_ACTIONS
} from '../src/common/config/constants.js';
console.log('--- Testing Standardized Offboarding Utilities ---');
// 1. Test validateOffboardingAction
console.log('Testing validateOffboardingAction...');
assert.deepEqual(validateOffboardingAction(OFFBOARDING_ACTIONS.APPROVE, ''), { valid: true });
assert.deepEqual(validateOffboardingAction(OFFBOARDING_ACTIONS.SEND_BACK, 'Short'), { valid: true }); // 'Short' is 5 chars
assert.equal(validateOffboardingAction(OFFBOARDING_ACTIONS.SEND_BACK, 'No').valid, false);
assert.equal(validateOffboardingAction(OFFBOARDING_ACTIONS.REVOKE, '').valid, false);
assert.equal(validateOffboardingAction(OFFBOARDING_ACTIONS.REJECT, '').valid, true); // Remarks not mandatory for reject in current util choice
console.log('✓ validateOffboardingAction passed.');
// 2. Test getPreviousStage - Termination
console.log('Testing getPreviousStage (Termination)...');
assert.equal(getPreviousStage(REQUEST_TYPES.TERMINATION, TERMINATION_STAGES.RBM_REVIEW), TERMINATION_STAGES.SUBMITTED);
assert.equal(getPreviousStage(REQUEST_TYPES.TERMINATION, TERMINATION_STAGES.ZBH_REVIEW), TERMINATION_STAGES.RBM_REVIEW);
assert.equal(getPreviousStage(REQUEST_TYPES.TERMINATION, TERMINATION_STAGES.TERMINATED), TERMINATION_STAGES.LEGAL_LETTER);
console.log('✓ Termination stage resolution passed.');
// 3. Test getPreviousStage - Resignation
console.log('Testing getPreviousStage (Resignation)...');
assert.equal(getPreviousStage(REQUEST_TYPES.RESIGNATION, RESIGNATION_STAGES.RBM), RESIGNATION_STAGES.ASM);
assert.equal(getPreviousStage(REQUEST_TYPES.RESIGNATION, RESIGNATION_STAGES.ZBH), RESIGNATION_STAGES.RBM);
assert.equal(getPreviousStage(REQUEST_TYPES.RESIGNATION, RESIGNATION_STAGES.FNF_INITIATED), RESIGNATION_STAGES.AWAITING_FNF);
assert.equal(getPreviousStage(REQUEST_TYPES.RESIGNATION, RESIGNATION_STAGES.DD_ADMIN), RESIGNATION_STAGES.LEGAL);
assert.equal(getPreviousStage(REQUEST_TYPES.RESIGNATION, RESIGNATION_STAGES.COMPLETED), RESIGNATION_STAGES.FNF_INITIATED);
console.log('✓ Resignation stage resolution passed.');
// 4. Test getPreviousStage - Constitutional
console.log('Testing getPreviousStage (Constitutional)...');
assert.equal(getPreviousStage(REQUEST_TYPES.CONSTITUTIONAL, CONSTITUTIONAL_STAGES.ASM_REVIEW), CONSTITUTIONAL_STAGES.SUBMITTED);
assert.equal(getPreviousStage(REQUEST_TYPES.CONSTITUTIONAL, CONSTITUTIONAL_STAGES.ZM_RBM_REVIEW), CONSTITUTIONAL_STAGES.ASM_REVIEW);
assert.equal(getPreviousStage(REQUEST_TYPES.CONSTITUTIONAL, CONSTITUTIONAL_STAGES.COMPLETED), CONSTITUTIONAL_STAGES.LEGAL_REVIEW);
console.log('✓ Constitutional stage resolution passed.');
// 5. Test getOffboardingAuditAction mapping
console.log('Testing getOffboardingAuditAction...');
assert.equal(getOffboardingAuditAction('Sent Back', REQUEST_TYPES.TERMINATION), AUDIT_ACTIONS.UPDATED);
assert.equal(getOffboardingAuditAction('Revoke', REQUEST_TYPES.RESIGNATION), AUDIT_ACTIONS.UPDATED);
assert.equal(getOffboardingAuditAction('Approve', REQUEST_TYPES.CONSTITUTIONAL), AUDIT_ACTIONS.APPROVED);
assert.equal(getOffboardingAuditAction('REJECT', REQUEST_TYPES.TERMINATION), AUDIT_ACTIONS.REJECTED);
console.log('✓ Audit action mapping passed.');
console.log('\nALL STANDARDIZATION UTILITY CHECKS PASSED SUCCESSFULLY.');

View File

@ -108,6 +108,9 @@ import createWorknote from './activity/Worknote.js';
import createWorkNoteAttachment from './activity/WorkNoteAttachment.js'; import createWorkNoteAttachment from './activity/WorkNoteAttachment.js';
import createWorkNoteTag from './activity/WorkNoteTag.js'; import createWorkNoteTag from './activity/WorkNoteTag.js';
// System
import createMigration from './system/Migration.js';
const env = process.env.NODE_ENV || 'development'; const env = process.env.NODE_ENV || 'development';
const dbConfig = config[env]; const dbConfig = config[env];
@ -233,6 +236,9 @@ db.StageApprovalPolicy = createStageApprovalPolicy(sequelize);
db.StageApprovalAction = createStageApprovalAction(sequelize); db.StageApprovalAction = createStageApprovalAction(sequelize);
db.SystemConfiguration = createSystemConfiguration(sequelize); db.SystemConfiguration = createSystemConfiguration(sequelize);
// Batch 9: System internals
db.Migration = createMigration(sequelize);
// Define associations // Define associations
Object.keys(db).forEach((modelName) => { Object.keys(db).forEach((modelName) => {
if (db[modelName].associate) { if (db[modelName].associate) {

View File

@ -0,0 +1,73 @@
import { Model, DataTypes, Sequelize } from 'sequelize';
/**
* Migration
* -----------
* Tracks which database migrations have been applied to this environment.
*
* Workflow:
* - `npm run migrate` -> destructive fresh sync (drops everything and
* recreates schema from Sequelize models).
* - `npm run migrate:baseline` -> stamps every existing migration file under
* `scripts/migrations/` as already-applied
* (use this immediately after a fresh sync so
* future incremental runs don't re-execute
* them on top of an already-correct schema).
* - `npm run migrate:up` -> applies any migration file under
* `scripts/migrations/` that is not yet
* recorded in this table.
* - `npm run migrate:status` -> lists applied vs pending migrations.
* - `npm run migrate:create` -> scaffolds a new `<ts>_<name>.ts` file.
*
* `name` is the canonical filename (without extension) so it remains stable
* even if the file is moved between directories.
*/
export interface MigrationAttributes {
id: string;
name: string;
appliedAt: Date;
checksum: string | null;
}
export interface MigrationInstance
extends Model<MigrationAttributes>,
MigrationAttributes { }
export default (sequelize: Sequelize) => {
const Migration = sequelize.define<MigrationInstance>(
'Migration',
{
id: {
type: DataTypes.UUID,
defaultValue: DataTypes.UUIDV4,
primaryKey: true
},
name: {
type: DataTypes.STRING(255),
allowNull: false,
unique: true
},
appliedAt: {
type: DataTypes.DATE,
allowNull: false,
defaultValue: DataTypes.NOW
},
checksum: {
// Optional SHA-256 of the migration file contents, useful to detect
// edits to an already-applied migration during code review.
type: DataTypes.STRING(64),
allowNull: true
}
},
{
tableName: 'migrations',
timestamps: false,
indexes: [
{ unique: true, fields: ['name'] },
{ fields: ['appliedAt'] }
]
}
);
return Migration;
};

View File

@ -784,23 +784,12 @@ export const takeAction = async (req: AuthRequest, res: Response) => {
newCurrentStage = RELOCATION_STAGES.REJECTED; newCurrentStage = RELOCATION_STAGES.REJECTED;
} }
// SRS §12.2.8 — enforce mandatory document submission + verification before late-stage approvals // SRS §12.2.8 — document upload/verification is tracked for readiness display
if ( // (see getRelocationDocumentReadiness + progress calculation) but is NOT a hard
normalizedAction === 'APPROVE' && // blocker on NBH Approval / Legal Clearance. Per business decision, these are
( // senior approval authorities who retain discretion to proceed even when the
request.currentStage === RELOCATION_STAGES.NBH_APPROVAL || // readiness panel still shows missing uploads or pending verifications. The
request.currentStage === RELOCATION_STAGES.LEGAL_CLEARANCE // readiness data continues to be surfaced on the UI so gaps remain visible.
)
) {
const readiness = getRelocationDocumentReadiness(request.documents || []);
if (readiness.missingUploads.length || readiness.pendingVerification.length) {
return res.status(400).json({
success: false,
message: 'Mandatory relocation documents are incomplete or pending verification.',
readiness
});
}
}
let newProgress = request.progressPercentage; let newProgress = request.progressPercentage;
if (normalizedAction === 'APPROVE') { if (normalizedAction === 'APPROVE') {

View File

@ -59,25 +59,69 @@ async function apiRequest(endpoint, method = 'GET', body = null, token = null) {
return data; return data;
} }
async function login(email) { async function login(email, password = PASSWORD) {
if (!login.cache) login.cache = {}; if (!login.cache) login.cache = {};
if (login.cache[email]) return login.cache[email]; if (login.cache[email]) return login.cache[email];
const data = await apiRequest('/auth/login', 'POST', { email, password: PASSWORD }); const data = await apiRequest('/auth/login', 'POST', { email, password });
login.cache[email] = data.token; login.cache[email] = data.token;
return login.cache[email]; return login.cache[email];
} }
async function discoverDealer(adminToken, preferredEmail) {
if (preferredEmail) {
try {
console.log(`Attempting login for dealer: ${preferredEmail}...`);
const token = await login(preferredEmail, 'Dealer@123');
return { dealerEmail: preferredEmail, dealerToken: token };
} catch (e) {
console.log(`Provided dealer login failed (${e.message}). Searching for an active onboarded dealer...`);
}
} else {
console.log('No --dealerEmail supplied. Searching for an active onboarded dealer...');
}
const appsRes = await apiRequest('/onboarding/applications', 'GET', null, adminToken);
const apps = appsRes?.data || [];
const onboarded = apps.filter(a => String(a.overallStatus || a.status || '').toLowerCase() === 'onboarded');
if (!onboarded.length) {
throw new Error('No onboarded dealers found in the system. Run trigger-workflow.js first to onboard a dealer, then retry.');
}
for (const app of onboarded) {
if (!app.email) continue;
try {
process.stdout.write(`Testing login for ${app.email}... `);
const data = await apiRequest('/auth/login', 'POST', { email: app.email, password: 'Dealer@123' });
// /auth/login only returns a token when the account is Active; inactive accounts
// are rejected upstream with HTTP 403 (caught below).
console.log('SUCCESS');
login.cache[app.email] = data.token;
return { dealerEmail: app.email, dealerToken: data.token };
} catch (err) {
console.log('FAILED');
}
}
throw new Error(
`Found ${onboarded.length} onboarded dealer(s), but none could be logged into with Dealer@123. ` +
`Either an active dealer doesn't exist, or the password has been rotated. ` +
`Pass --dealerEmail explicitly or activate one of: ${onboarded.map(a => a.email).filter(Boolean).join(', ')}.`
);
}
const delay = (ms = STEP_DELAY_MS) => new Promise(res => setTimeout(res, ms)); const delay = (ms = STEP_DELAY_MS) => new Promise(res => setTimeout(res, ms));
async function run() { async function run() {
try { try {
console.log('--- STARTING CONSTITUTIONAL CHANGE E2E FLOW ---'); console.log('--- STARTING CONSTITUTIONAL CHANGE E2E FLOW ---');
if (!EMAILS.DEALER) {
throw new Error('Missing --dealerEmail. This script requires an existing dealer user email.');
}
console.log(`[STEP 0] Logging in as Dealer: ${EMAILS.DEALER}...`); const adminTokenEarly = await login(EMAILS.DD_ADMIN);
const dealerToken = await login(EMAILS.DEALER); const discovered = await discoverDealer(adminTokenEarly, EMAILS.DEALER);
EMAILS.DEALER = discovered.dealerEmail;
const dealerToken = discovered.dealerToken;
console.log(`[STEP 0] Logged in as Dealer: ${EMAILS.DEALER}.`);
let requestId = args.requestId; let requestId = args.requestId;
if (!requestId) { if (!requestId) {
@ -95,19 +139,7 @@ async function run() {
console.log(`[STEP 1] Resuming request: ${requestId}`); console.log(`[STEP 1] Resuming request: ${requestId}`);
} }
// Sequence of users taking actions to advance stages const adminToken = adminTokenEarly;
const approvalSequence = [
{ name: 'ASM', email: EMAILS.ASM },
{ name: 'ZM/RBM', email: EMAILS.RBM_L1 },
{ name: 'ZBH', email: EMAILS.ZBH },
{ name: 'DD Lead', email: EMAILS.DD_LEAD },
{ name: 'DD Head', email: EMAILS.DD_HEAD },
{ name: 'NBH', email: EMAILS.NBH },
{ name: 'Legal Review', email: EMAILS.LEGAL },
{ name: 'Legal Finalize', email: EMAILS.LEGAL }
];
const adminToken = await login(EMAILS.DD_ADMIN);
const asmFromMapping = args.asmEmail || await resolveDealerAsmEmail(adminToken, EMAILS.DEALER); const asmFromMapping = args.asmEmail || await resolveDealerAsmEmail(adminToken, EMAILS.DEALER);
if (asmFromMapping) { if (asmFromMapping) {
EMAILS.ASM = asmFromMapping; EMAILS.ASM = asmFromMapping;
@ -115,15 +147,44 @@ async function run() {
} else { } else {
console.log(`[WARN] Dealer-level ASM not found. Falling back to default ASM email: ${EMAILS.ASM}`); console.log(`[WARN] Dealer-level ASM not found. Falling back to default ASM email: ${EMAILS.ASM}`);
} }
const current = await apiRequest(`/self-service/constitutional/${requestId}`, 'GET', null, adminToken);
const currentStage = current?.request?.currentStage;
const stageOrder = ['Submitted', 'ASM Review', 'ZM/RBM Review', 'ZBH Review', 'DD Lead Review', 'DD Head Review', 'NBH Approval', 'Legal Review', 'Completed'];
const startIndex = Math.max(0, stageOrder.indexOf(currentStage));
let currentStep = 2 + startIndex; // Stage → actor mapping mirrors constitutional.controller.ts stageRoleMap.
for (let i = startIndex; i < approvalSequence.length; i++) { // Driving the loop off the live currentStage (not a pre-computed index) keeps
const actor = approvalSequence[i]; // the script self-healing if the workflow shape ever changes.
console.log(`[STEP ${currentStep}] ${actor.name} (${actor.email}) processing approval...`); const ACTOR_BY_STAGE = {
'ASM Review': { name: 'ASM', email: EMAILS.ASM },
'ZM/RBM Review': { name: 'ZM/RBM', email: EMAILS.RBM_L1 },
'ZBH Review': { name: 'ZBH', email: EMAILS.ZBH },
'DD Lead Review': { name: 'DD Lead', email: EMAILS.DD_LEAD },
'DD Head Review': { name: 'DD Head', email: EMAILS.DD_HEAD },
'NBH Approval': { name: 'NBH', email: EMAILS.NBH },
'Legal Review': { name: 'Legal', email: EMAILS.LEGAL }
};
let currentStep = 2;
const MAX_ITER = 12; // safety cap
for (let i = 0; i < MAX_ITER; i++) {
const detailsRes = await apiRequest(`/self-service/constitutional/${requestId}`, 'GET', null, adminToken);
const stage = detailsRes?.request?.currentStage;
const status = detailsRes?.request?.status;
if (stage === 'Completed' || status === 'Completed') {
console.log(`[STEP ${currentStep}] SUCCESS: Request reached COMPLETED state.`);
break;
}
if (stage === 'Rejected' || status === 'Rejected' || stage === 'Revoked' || status === 'Revoked') {
throw new Error(`Constitutional change ended in ${stage || status} state before completion.`);
}
const actor = ACTOR_BY_STAGE[stage];
if (!actor) {
throw new Error(`No actor mapping found for stage: ${stage}`);
}
if (!actor.email) {
throw new Error(`Missing approver email for stage ${stage} (actor ${actor.name}).`);
}
console.log(`[STEP ${currentStep}] ${actor.name} (${actor.email}) processing ${stage}...`);
const token = await login(actor.email); const token = await login(actor.email);
const res = await apiRequest(`/self-service/constitutional/${requestId}/action`, 'POST', { const res = await apiRequest(`/self-service/constitutional/${requestId}/action`, 'POST', {
action: 'Approve', action: 'Approve',
@ -136,7 +197,7 @@ async function run() {
console.log('[FINAL STEP] Verifying Completion Status...'); console.log('[FINAL STEP] Verifying Completion Status...');
const finalDetails = await apiRequest(`/self-service/constitutional/${requestId}`, 'GET', null, adminToken); const finalDetails = await apiRequest(`/self-service/constitutional/${requestId}`, 'GET', null, adminToken);
if (finalDetails.request.status === 'Completed' || finalDetails.request.currentStage === 'Completed') { if (finalDetails.request.status === 'Completed' || finalDetails.request.currentStage === 'Completed') {
console.log(`[STEP ${currentStep}] SUCCESS: Request reached COMPLETED state.`); console.log(`[STEP ${currentStep}] SUCCESS: Request reached COMPLETED state.`);
} else { } else {

View File

@ -4,6 +4,7 @@ const args = Object.fromEntries(
.map(([k, v]) => [k, v ?? 'true']) .map(([k, v]) => [k, v ?? 'true'])
); );
const BASE_URL = args.baseUrl || process.env.BASE_URL || 'http://localhost:5000/api'; const BASE_URL = args.baseUrl || process.env.BASE_URL || 'http://localhost:5000/api';
// const BASE_URL = 'https://dealeronboarding-uat.royalenfield.com/api';
const PASSWORD = 'Admin@123'; const PASSWORD = 'Admin@123';
const STEP_DELAY_MS = Number(args.delayMs || 500); const STEP_DELAY_MS = Number(args.delayMs || 500);
const SHOULD_SKIP_CLEARANCES = String(args.skipClearances || 'false') === 'true'; const SHOULD_SKIP_CLEARANCES = String(args.skipClearances || 'false') === 'true';

View File

@ -312,18 +312,23 @@ async function run() {
if (!terminationId) { if (!terminationId) {
log(1, 'Creating termination (ASM)...'); log(1, 'Creating termination (ASM)...');
const asmToken = await login(EMAILS.ASM); const asmToken = await login(EMAILS.ASM);
const createRes = await apiRequest(
'/termination', // Backend (termination.controller.ts) requires at least one .ppt/.pptx
'POST', // file for non-Super-Admin initiators, sent as multipart "files".
{ const form = new FormData();
dealerId: targetDealer.id, form.append('dealerId', targetDealer.id);
category: args.category || 'Performance', form.append('category', args.category || 'Performance');
reason: args.reason || 'Consistently failed to meet commitment targets.', form.append('reason', args.reason || 'Consistently failed to meet commitment targets.');
proposedLwd: new Date().toISOString().split('T')[0], form.append('proposedLwd', new Date().toISOString().split('T')[0]);
comments: 'E2E termination — follows UI stage order (no stacked partial approvals).' form.append('comments', 'E2E termination — follows UI stage order (no stacked partial approvals).');
},
asmToken const dummyPptxBytes = Buffer.from('E2E placeholder presentation');
); const dummyPptxBlob = new Blob([dummyPptxBytes], {
type: 'application/vnd.openxmlformats-officedocument.presentationml.presentation'
});
form.append('files', dummyPptxBlob, 'e2e-termination-presentation.pptx');
const createRes = await apiRequest('/termination', 'POST', form, asmToken, true);
terminationId = createRes.termination.id; terminationId = createRes.termination.id;
log(1, `Created: ${terminationId} (${args.category || 'Performance'})`); log(1, `Created: ${terminationId} (${args.category || 'Performance'})`);
} else { } else {

View File

@ -10,6 +10,7 @@ const args = Object.fromEntries(
.map(([k, v]) => [k, v ?? 'true']) .map(([k, v]) => [k, v ?? 'true'])
); );
const BASE_URL = args.baseUrl || process.env.BASE_URL || 'http://localhost:5000/api'; const BASE_URL = args.baseUrl || process.env.BASE_URL || 'http://localhost:5000/api';
// const BASE_URL = 'https://dealeronboarding-uat.royalenfield.com/api';
const PASSWORD = 'Admin@123'; const PASSWORD = 'Admin@123';
const OTP = '123456'; const OTP = '123456';
const STEP_DELAY_MS = Number(args.delayMs || 1000); const STEP_DELAY_MS = Number(args.delayMs || 1000);
@ -381,204 +382,205 @@ async function triggerWorkflow() {
log(7, 'FDD Milestone Complete.'); log(7, 'FDD Milestone Complete.');
await delay(); await delay();
// log(7.4, 'Uploading mandatory pre-LOI evidence documents (CIBIL / Site / Bank / GST / PAN)...'); log(7.4, 'Uploading mandatory pre-LOI evidence documents (CIBIL / Site / Bank / GST / PAN)...');
// const preLoiDocs = ['CIBIL Report', 'Proposed Site City Map', 'Bank Statement', 'GST Certificate', 'PAN Card']; const preLoiDocs = ['CIBIL Report', 'Proposed Site City Map', 'Bank Statement', 'GST Certificate', 'PAN Card'];
// for (const doc of preLoiDocs) { for (const doc of preLoiDocs) {
// await mockUploadDocument(applicationUUID, adminToken, doc); await mockUploadDocument(applicationUUID, adminToken, doc);
// } }
// await delay(1000); await delay(1000);
// // 7.5 LOI APPROVAL (multi-approver: DD-Head + NBH) // 7.5 LOI APPROVAL (multi-approver: DD-Head + NBH)
// log(7.5, 'Requesting LOI and collecting required approvals...'); log(7.5, 'Requesting LOI and collecting required approvals...');
// const loiRes = await apiRequest('/loi/request', 'POST', { applicationId: applicationUUID }, adminToken); const loiRes = await apiRequest('/loi/request', 'POST', { applicationId: applicationUUID }, adminToken);
// const loiRequestId = loiRes.data.id; const loiRequestId = loiRes.data.id;
// log(7.5, `LOI Request created (ID: ${loiRequestId})`); log(7.5, `LOI Request created (ID: ${loiRequestId})`);
// // DD-Head Approval // DD-Head Approval
// await apiRequest(`/loi/request/${loiRequestId}/approve`, 'POST', { await apiRequest(`/loi/request/${loiRequestId}/approve`, 'POST', {
// action: 'Approved', action: 'Approved',
// remarks: 'DD-Head authorization for LOI' remarks: 'DD-Head authorization for LOI'
// }, headToken); }, headToken);
// // NBH Approval — final approver flips overallStatus to "Security Details" // NBH Approval — final approver flips overallStatus to "Security Details"
// // (LOI Approved → moves into the Security Deposit corridor before LOI Issue). // (LOI Approved → moves into the Security Deposit corridor before LOI Issue).
// await apiRequest(`/loi/request/${loiRequestId}/approve`, 'POST', { await apiRequest(`/loi/request/${loiRequestId}/approve`, 'POST', {
// action: 'Approved', action: 'Approved',
// remarks: 'NBH authorization for LOI' remarks: 'NBH authorization for LOI'
// }, nbhToken); }, nbhToken);
// log(7.5, 'LOI fully approved. Backend transitioned to Security Deposit corridor.'); log(7.5, 'LOI fully approved. Backend transitioned to Security Deposit corridor.');
// await delay(); await delay();
// // 8. SECURITY DEPOSIT — Finance verifies advance payment. // 8. SECURITY DEPOSIT — Finance verifies advance payment.
// // Backend keeps app at "Security Details" (no auto-jump to LOI Issued) — admin must // Backend keeps app at "Security Details" (no auto-jump to LOI Issued) — admin must
// // (a) upload LOI documents and (b) explicitly transition. See loa.controller.ts. // (a) upload LOI documents and (b) explicitly transition. See loa.controller.ts.
// log(8, 'Finance Verifying SECURITY_DEPOSIT (₹5L advance)...'); log(8, 'Finance Verifying SECURITY_DEPOSIT (₹5L advance)...');
// const financeToken = await login(EMAILS.FINANCE); const financeToken = await login(EMAILS.FINANCE);
// await apiRequest('/loa/security-deposit', 'POST', { await apiRequest('/loa/security-deposit', 'POST', {
// applicationId: applicationUUID, applicationId: applicationUUID,
// amount: 500000, amount: 500000,
// paymentReference: `PAY-SD-${Date.now()}`, paymentReference: `PAY-SD-${Date.now()}`,
// depositType: 'SECURITY_DEPOSIT', depositType: 'SECURITY_DEPOSIT',
// status: 'Verified' status: 'Verified'
// }, financeToken); }, financeToken);
// log(8, 'Security Deposit Verified.'); log(8, 'Security Deposit Verified.');
// await delay(); await delay();
// // 8.5 LOI DOCUMENTS COLLECTION // 8.5 LOI DOCUMENTS COLLECTION
// // Per business rule (and the new UI "LOI Documents" stage), the dealer-side LOI artefacts // Per business rule (and the new UI "LOI Documents" stage), the dealer-side LOI artefacts
// // must be uploaded BEFORE the admin transitions the application to "LOI Issued". // must be uploaded BEFORE the admin transitions the application to "LOI Issued".
// log(8.5, 'Uploading LOI Documents (Letter of Intent + Signed LOI) prior to LOI Issuance...'); log(8.5, 'Uploading LOI Documents (Letter of Intent + Signed LOI) prior to LOI Issuance...');
// const loiDocs = ['Letter of Intent', 'Signed LOI']; const loiDocs = ['Letter of Intent', 'Signed LOI'];
// for (const doc of loiDocs) { for (const doc of loiDocs) {
// await mockUploadDocument(applicationUUID, adminToken, doc); await mockUploadDocument(applicationUUID, adminToken, doc);
// } }
// log(8.5, 'LOI Documents uploaded. Ready for LOI Issued transition.'); log(8.5, 'LOI Documents uploaded. Ready for LOI Issued transition.');
// await delay(); await delay();
// // 9. LOI ISSUE — explicit admin transition + LOI document generation. // 9. LOI ISSUE — explicit admin transition + LOI document generation.
// let statusBeforeCodeGen = await getApplicationStatus(applicationUUID, adminToken); let statusBeforeCodeGen = await getApplicationStatus(applicationUUID, adminToken);
// log(9, `Current status before LOI Issued transition: ${statusBeforeCodeGen}`); log(9, `Current status before LOI Issued transition: ${statusBeforeCodeGen}`);
// log(9, 'Ensuring mandatory PAN/GST/Bank fields are populated...'); log(9, 'Ensuring mandatory PAN/GST/Bank fields are populated...');
// await ensureMandatoryCodeGenFields(applicationUUID, adminToken); await ensureMandatoryCodeGenFields(applicationUUID, adminToken);
// await delay(300); await delay(300);
// if (statusBeforeCodeGen === 'Security Deposit' || statusBeforeCodeGen === 'Security Details') { if (statusBeforeCodeGen === 'Security Deposit' || statusBeforeCodeGen === 'Security Details') {
// log(9, 'Applying admin transition: Security Deposit -> LOI Issued...'); log(9, 'Applying admin transition: Security Deposit -> LOI Issued...');
// await apiRequest(`/onboarding/applications/${applicationUUID}/status`, 'PUT', { await apiRequest(`/onboarding/applications/${applicationUUID}/status`, 'PUT', {
// status: 'LOI Issued', status: 'LOI Issued',
// stage: 'LOI', stage: 'LOI',
// reason: 'LOI documents collected and verified. Releasing LOI Issued milestone.' reason: 'LOI documents collected and verified. Releasing LOI Issued milestone.'
// }, adminToken); }, adminToken);
// await delay(); await delay();
// statusBeforeCodeGen = await getApplicationStatus(applicationUUID, adminToken); statusBeforeCodeGen = await getApplicationStatus(applicationUUID, adminToken);
// log(9, `Status after admin transition: ${statusBeforeCodeGen}`); log(9, `Status after admin transition: ${statusBeforeCodeGen}`);
// } }
// // generateDocument bridges LOI Issued -> Dealer Code Generation (loi.controller.ts:459). // generateDocument bridges LOI Issued -> Dealer Code Generation (loi.controller.ts:459).
// log(9, 'Generating final LOI document (bridges to Dealer Code Generation)...'); // Route is POST /loi/request/:requestId/generate — requestId goes in the URL, not the body.
// await apiRequest('/loi/generate-document', 'POST', { requestId: loiRequestId }, adminToken); log(9, 'Generating final LOI document (bridges to Dealer Code Generation)...');
// await delay(); await apiRequest(`/loi/request/${loiRequestId}/generate`, 'POST', {}, adminToken);
// statusBeforeCodeGen = await getApplicationStatus(applicationUUID, adminToken); await delay();
// log(9, `Status after LOI generateDocument: ${statusBeforeCodeGen}`); statusBeforeCodeGen = await getApplicationStatus(applicationUUID, adminToken);
log(9, `Status after LOI generateDocument: ${statusBeforeCodeGen}`);
// if (statusBeforeCodeGen !== 'LOI Issued' && statusBeforeCodeGen !== 'Dealer Code Generation') { if (statusBeforeCodeGen !== 'LOI Issued' && statusBeforeCodeGen !== 'Dealer Code Generation') {
// throw new Error(`Cannot generate codes: expected LOI Issued/Dealer Code Generation, got ${statusBeforeCodeGen}`); throw new Error(`Cannot generate codes: expected LOI Issued/Dealer Code Generation, got ${statusBeforeCodeGen}`);
// } }
// log(9, 'Admin Generating SAP Dealer Codes...'); log(9, 'Admin Generating SAP Dealer Codes...');
// await apiRequest(`/onboarding/applications/${applicationUUID}/generate-codes`, 'POST', {}, adminToken); await apiRequest(`/onboarding/applications/${applicationUUID}/generate-codes`, 'POST', {}, adminToken);
// log(9, 'Dealer Codes Generated.'); log(9, 'Dealer Codes Generated.');
// await delay(); await delay();
// // 10. FIRST FILL (POST CODE-GENERATION) // 10. FIRST FILL (POST CODE-GENERATION)
// log(10, 'Finance Verifying FIRST FILL (₹15L)...'); log(10, 'Finance Verifying FIRST FILL (₹15L)...');
// await apiRequest('/loa/security-deposit', 'POST', { await apiRequest('/loa/security-deposit', 'POST', {
// applicationId: applicationUUID, applicationId: applicationUUID,
// amount: 1500000, amount: 1500000,
// paymentReference: `PAY-FF-${Date.now()}`, paymentReference: `PAY-FF-${Date.now()}`,
// depositType: 'FIRST_FILL', depositType: 'FIRST_FILL',
// status: 'Verified' status: 'Verified'
// }, financeToken); }, financeToken);
// log(10, 'First Fill Verified.'); log(10, 'First Fill Verified.');
// await delay(); await delay();
// // 11. ADMIN UPDATING STATUTORY & BANK DETAILS // 11. ADMIN UPDATING STATUTORY & BANK DETAILS
// log(11, 'Admin Updating Statutory & Bank Details for LOA Approval Gate...'); log(11, 'Admin Updating Statutory & Bank Details for LOA Approval Gate...');
// await apiRequest(`/onboarding/applications/${applicationUUID}`, 'PUT', { await apiRequest(`/onboarding/applications/${applicationUUID}`, 'PUT', {
// accountHolderName: 'Ramesh Automobiles Private Limited', accountHolderName: 'Ramesh Automobiles Private Limited',
// panNumber: 'ABCDE1234F', panNumber: 'ABCDE1234F',
// gstNumber: '07ABCDE1234F1Z5', gstNumber: '07ABCDE1234F1Z5',
// bankName: 'HDFC Bank', bankName: 'HDFC Bank',
// accountNumber: '50100223344556', accountNumber: '50100223344556',
// ifscCode: 'HDFC0001234' ifscCode: 'HDFC0001234'
// }, adminToken); }, adminToken);
// log(11, 'Statutory & Bank details updated.'); log(11, 'Statutory & Bank details updated.');
// await delay(); await delay();
// // 12. FINAL LOA APPROVAL // 12. FINAL LOA APPROVAL
// log(12, 'NBH & Head Approving Final LOA...'); log(12, 'NBH & Head Approving Final LOA...');
// const loaRes = await apiRequest('/loa/request', 'POST', { applicationId: applicationUUID }, headToken); const loaRes = await apiRequest('/loa/request', 'POST', { applicationId: applicationUUID }, headToken);
// const finalLoaRequestId = loaRes.data.id; const finalLoaRequestId = loaRes.data.id;
// await apiRequest(`/loa/request/${finalLoaRequestId}/approve`, 'POST', { await apiRequest(`/loa/request/${finalLoaRequestId}/approve`, 'POST', {
// action: 'Approved', action: 'Approved',
// remarks: 'Head Authorization (Level 1)' remarks: 'Head Authorization (Level 1)'
// }, headToken); }, headToken);
// await apiRequest(`/loa/request/${finalLoaRequestId}/approve`, 'POST', { await apiRequest(`/loa/request/${finalLoaRequestId}/approve`, 'POST', {
// action: 'Approved', action: 'Approved',
// remarks: 'NBH Approval (Level 2)' remarks: 'NBH Approval (Level 2)'
// }, nbhToken); }, nbhToken);
// log(12, 'LOA Fully Approved.'); log(12, 'LOA Fully Approved.');
// await delay(); await delay();
// // 13. EOR (EVIDENCE OF READINESS) CHECKLIST VERIFICATION // 13. EOR (EVIDENCE OF READINESS) CHECKLIST VERIFICATION
// log(13, 'Admin Initializing EOR Checklist (100% Readiness Requirement)...'); log(13, 'Admin Initializing EOR Checklist (100% Readiness Requirement)...');
// const eorInit = await apiRequest('/eor', 'POST', { applicationId: applicationUUID }, adminToken); const eorInit = await apiRequest('/eor', 'POST', { applicationId: applicationUUID }, adminToken);
// const checklistId = eorInit.data.id; const checklistId = eorInit.data.id;
// log(13, `EOR Checklist Created (ID: ${checklistId})`); log(13, `EOR Checklist Created (ID: ${checklistId})`);
// log(13.1, 'Auditor Verifying all 12 mandatory EOR items as COMPLIANT...'); log(13.1, 'Auditor Verifying all 12 mandatory EOR items as COMPLIANT...');
// const eorItems = [ const eorItems = [
// { itemType: 'Sales', description: 'Sales Standards' }, { itemType: 'Sales', description: 'Sales Standards' },
// { itemType: 'Service', description: 'Service & Spares' }, { itemType: 'Service', description: 'Service & Spares' },
// { itemType: 'IT', description: 'DMS infra' }, { itemType: 'IT', description: 'DMS infra' },
// { itemType: 'Training', description: 'Manpower Training' }, { itemType: 'Training', description: 'Manpower Training' },
// { itemType: 'Statutory', description: 'Trade certificate with test ride bikes registration' }, { itemType: 'Statutory', description: 'Trade certificate with test ride bikes registration' },
// { itemType: 'Statutory', description: 'GST certificate including Accessories & Apparels billing' }, { itemType: 'Statutory', description: 'GST certificate including Accessories & Apparels billing' },
// { itemType: 'Finance', description: 'Inventory Funding' }, { itemType: 'Finance', description: 'Inventory Funding' },
// { itemType: 'IT', description: 'Virtual code availability' }, { itemType: 'IT', description: 'Virtual code availability' },
// { itemType: 'Finance', description: 'Vendor payments' }, { itemType: 'Finance', description: 'Vendor payments' },
// { itemType: 'Marketing', description: 'Details for website submission' }, { itemType: 'Marketing', description: 'Details for website submission' },
// { itemType: 'Insurance', description: 'Infra Insurance both Showroom and Service center' }, { itemType: 'Insurance', description: 'Infra Insurance both Showroom and Service center' },
// { itemType: 'IT', description: 'Auto ordering' } { itemType: 'IT', description: 'Auto ordering' }
// ]; ];
// for (const item of eorItems) { for (const item of eorItems) {
// process.stdout.write(`.`); process.stdout.write(`.`);
// await apiRequest(`/eor/item/${checklistId}`, 'POST', { await apiRequest(`/eor/item/${checklistId}`, 'POST', {
// ...item, ...item,
// isCompliant: true, isCompliant: true,
// remarks: 'Verified by Auditor - Compliant' remarks: 'Verified by Auditor - Compliant'
// }, adminToken); }, adminToken);
// } }
// console.log('\n[STEP 13.1] All EOR items marked as compliant.'); console.log('\n[STEP 13.1] All EOR items marked as compliant.');
// log(13.2, 'Auditor Submitting Final EOR Audit...'); log(13.2, 'Auditor Submitting Final EOR Audit...');
// await apiRequest(`/eor/audit/${checklistId}`, 'POST', { await apiRequest(`/eor/audit/${checklistId}`, 'POST', {
// status: 'Completed', status: 'Completed',
// overallComments: 'Dealer is 100% ready for inauguration. All infra and statutory items verified.' overallComments: 'Dealer is 100% ready for inauguration. All infra and statutory items verified.'
// }, adminToken); }, adminToken);
// const finalAppStatus = await apiRequest(`/onboarding/applications/${applicationUUID}`, 'GET', null, adminToken); const finalAppStatus = await apiRequest(`/onboarding/applications/${applicationUUID}`, 'GET', null, adminToken);
// log(13.2, `Application Status after EOR: ${finalAppStatus.data.overallStatus}`); log(13.2, `Application Status after EOR: ${finalAppStatus.data.overallStatus}`);
// await delay(); await delay();
// // 14. FINAL ONBOARDING // 14. FINAL ONBOARDING
// log(14, 'Admin Finalizing Dealer Onboarding...'); log(14, 'Admin Finalizing Dealer Onboarding...');
// await apiRequest('/dealers', 'POST', { applicationId: applicationUUID }, adminToken); await apiRequest('/dealers', 'POST', { applicationId: applicationUUID }, adminToken);
// await delay(); await delay();
// // 15. VERIFICATION // 15. VERIFICATION
// log(15, 'Verifying Dealer Record Creation...'); log(15, 'Verifying Dealer Record Creation...');
// const dealerRes = await apiRequest(`/dealers/application/${applicationUUID}`, 'GET', null, adminToken); const dealerRes = await apiRequest(`/dealers/application/${applicationUUID}`, 'GET', null, adminToken);
// if (!dealerRes.success || !dealerRes.data) { if (!dealerRes.success || !dealerRes.data) {
// throw new Error('Verification Failed: Dealer record not found after onboarding.'); throw new Error('Verification Failed: Dealer record not found after onboarding.');
// } }
// log(15, `Dealer Found: ${dealerRes.data.legalName} (${dealerRes.data.id})`); log(15, `Dealer Found: ${dealerRes.data.legalName} (${dealerRes.data.id})`);
// log(15.1, 'Verifying User Account Role Update...'); log(15.1, 'Verifying User Account Role Update...');
// const userRes = await apiRequest(`/admin/users`, 'GET', null, adminToken); const userRes = await apiRequest(`/admin/users`, 'GET', null, adminToken);
// const dealerUser = userRes.data.find(u => u.email === PROSPECT_EMAIL); const dealerUser = userRes.data.find(u => u.email === PROSPECT_EMAIL);
// if (!dealerUser || dealerUser.roleCode !== 'Dealer') { if (!dealerUser || dealerUser.roleCode !== 'Dealer') {
// throw new Error(`Verification Failed: User role not updated to 'Dealer'. Current role: ${dealerUser?.roleCode}`); throw new Error(`Verification Failed: User role not updated to 'Dealer'. Current role: ${dealerUser?.roleCode}`);
// } }
// log(15.1, `User role confirmed: ${dealerUser.roleCode}`); log(15.1, `User role confirmed: ${dealerUser.roleCode}`);
// log(15.2, '--- WORKFLOW COMPLETED SUCCESSFULLY! ---'); log(15.2, '--- WORKFLOW COMPLETED SUCCESSFULLY! ---');
// log(15.2, `The application ${applicationId} is now at 'ONBOARDED' status and Dealer profile is active.`); log(15.2, `The application ${applicationId} is now at 'ONBOARDED' status and Dealer profile is active.`);
} }
/** /**