Compare commits
No commits in common. "main" and "laxman_dev" have entirely different histories.
main
...
laxman_dev
File diff suppressed because one or more lines are too long
6
build/assets/index-DqVo88us.css
Normal file
6
build/assets/index-DqVo88us.css
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -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-CIW1_Mz_.js"></script>
|
<script type="module" crossorigin src="/assets/index-XdyJ-8da.js"></script>
|
||||||
<link rel="stylesheet" crossorigin href="/assets/index-CDNp5hMY.css">
|
<link rel="stylesheet" crossorigin href="/assets/index-DqVo88us.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
|
|||||||
@ -41,7 +41,7 @@ Ordered by impact. Update this file when items ship.
|
|||||||
|
|
||||||
## Verification checklist
|
## Verification checklist
|
||||||
|
|
||||||
1. `npm run migrate:up` (applies pending versioned migrations, including SLA schema)
|
1. `npx tsx scripts/migrate-sla-tracking-schema.ts` (once per DB if needed)
|
||||||
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
|
||||||
|
|||||||
@ -38,20 +38,19 @@ Or use **Master → SLA Configuration → Initialize defaults** in the UI.
|
|||||||
|
|
||||||
## DB note
|
## DB note
|
||||||
|
|
||||||
Schema for `sla_tracking` and `sla_notification_dispatches` is defined in the
|
If `sla_tracking.metadata` (or `entityType` / `entityId`) is missing on an older database, run:
|
||||||
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
|
||||||
npm run migrate:up
|
npx tsx scripts/migrate-sla-tracking-schema.ts
|
||||||
```
|
```
|
||||||
|
|
||||||
### 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 L1–L3, 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`.
|
Table `sla_notification_dispatches` records **one row per threshold per active track** (pre-breach reminder, breach, escalation L1–L3, repeat overdue window). Unique on `(trackingId, thresholdKey)` prevents duplicate emails even if the worker runs every minute.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npx tsx scripts/migrate-sla-notification-dispatches.ts
|
||||||
|
```
|
||||||
|
|
||||||
| `dispatchType` | `thresholdKey` example | Sends |
|
| `dispatchType` | `thresholdKey` example | Sends |
|
||||||
|----------------|------------------------|--------|
|
|----------------|------------------------|--------|
|
||||||
|
|||||||
@ -10,10 +10,7 @@
|
|||||||
"build": "tsc",
|
"build": "tsc",
|
||||||
"type-check": "tsc --noEmit",
|
"type-check": "tsc --noEmit",
|
||||||
"migrate": "tsx scripts/migrate.ts",
|
"migrate": "tsx scripts/migrate.ts",
|
||||||
"migrate:up": "tsx scripts/run-migrations.ts",
|
"migrate:sla-dispatches": "tsx scripts/migrate-sla-notification-dispatches.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",
|
||||||
|
|||||||
39
scripts/add-architecture-role.ts
Normal file
39
scripts/add-architecture-role.ts
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
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();
|
||||||
26
scripts/add-decision-column.ts
Normal file
26
scripts/add-decision-column.ts
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
|
||||||
|
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();
|
||||||
28
scripts/add-level3-enum.ts
Normal file
28
scripts/add-level3-enum.ts
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
|
||||||
|
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();
|
||||||
27
scripts/add-recovery-enum.ts
Normal file
27
scripts/add-recovery-enum.ts
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
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();
|
||||||
74
scripts/assign_south_delhi.ts
Normal file
74
scripts/assign_south_delhi.ts
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
|
||||||
|
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();
|
||||||
28
scripts/check-sla-dispatches.ts
Normal file
28
scripts/check-sla-dispatches.ts
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
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);
|
||||||
|
});
|
||||||
18
scripts/check-smtp-config.ts
Normal file
18
scripts/check-smtp-config.ts
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
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);
|
||||||
|
});
|
||||||
26
scripts/check_app.ts
Normal file
26
scripts/check_app.ts
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
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();
|
||||||
18
scripts/check_column.ts
Normal file
18
scripts/check_column.ts
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
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();
|
||||||
19
scripts/check_enum.ts
Normal file
19
scripts/check_enum.ts
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
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();
|
||||||
48
scripts/check_recent_app.ts
Normal file
48
scripts/check_recent_app.ts
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
|
||||||
|
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();
|
||||||
26
scripts/cleanup-interview-orphans.ts
Normal file
26
scripts/cleanup-interview-orphans.ts
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
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();
|
||||||
@ -1,106 +0,0 @@
|
|||||||
/**
|
|
||||||
* 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);
|
|
||||||
});
|
|
||||||
40
scripts/create-system-audit-log-table.ts
Normal file
40
scripts/create-system-audit-log-table.ts
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
/**
|
||||||
|
* 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();
|
||||||
63
scripts/debug-area-manager.ts
Normal file
63
scripts/debug-area-manager.ts
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
|
||||||
|
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();
|
||||||
34
scripts/debug-evaluations.ts
Normal file
34
scripts/debug-evaluations.ts
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
|
||||||
|
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();
|
||||||
20
scripts/debug_roles.ts
Normal file
20
scripts/debug_roles.ts
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
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();
|
||||||
53
scripts/delete-test-relocation.ts
Normal file
53
scripts/delete-test-relocation.ts
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
/**
|
||||||
|
* 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);
|
||||||
76
scripts/diagnose_associations.ts
Normal file
76
scripts/diagnose_associations.ts
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
|
||||||
|
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();
|
||||||
24
scripts/find_abhishek.ts
Normal file
24
scripts/find_abhishek.ts
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
|
||||||
|
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();
|
||||||
26
scripts/fix-asm-column.ts
Normal file
26
scripts/fix-asm-column.ts
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
|
||||||
|
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();
|
||||||
26
scripts/fix-remarks-column.ts
Normal file
26
scripts/fix-remarks-column.ts
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
|
||||||
|
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();
|
||||||
47
scripts/fix-stages-enum.ts
Normal file
47
scripts/fix-stages-enum.ts
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
|
||||||
|
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();
|
||||||
40
scripts/fix_south_delhi.ts
Normal file
40
scripts/fix_south_delhi.ts
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
|
||||||
|
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();
|
||||||
21
scripts/force-sync.ts
Normal file
21
scripts/force-sync.ts
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
|
||||||
|
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();
|
||||||
29
scripts/migrate-evaluation-schema.ts
Normal file
29
scripts/migrate-evaluation-schema.ts
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
|
||||||
|
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();
|
||||||
83
scripts/migrate-onboarding-documents-cleanup.ts
Normal file
83
scripts/migrate-onboarding-documents-cleanup.ts
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
/**
|
||||||
|
* 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();
|
||||||
74
scripts/migrate-relocation-schema.ts
Normal file
74
scripts/migrate-relocation-schema.ts
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
/**
|
||||||
|
* 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();
|
||||||
49
scripts/migrate-sla-notification-dispatches.ts
Normal file
49
scripts/migrate-sla-notification-dispatches.ts
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
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);
|
||||||
|
});
|
||||||
34
scripts/migrate-sla-tracking-schema.ts
Normal file
34
scripts/migrate-sla-tracking-schema.ts
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
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);
|
||||||
|
});
|
||||||
@ -1,89 +1,36 @@
|
|||||||
/**
|
/**
|
||||||
* Database Migration Script — destructive fresh sync.
|
* Database Migration Script
|
||||||
|
* Synchronizes all Sequelize models with the database (PostgreSQL).
|
||||||
|
* This script will DROP all existing tables and recreate them.
|
||||||
*
|
*
|
||||||
* Drops every table and recreates the schema from Sequelize model definitions
|
* Schema for modules such as constitutional change (ENUM values, partial unique indexes,
|
||||||
* in `src/database/models/`. After the fresh schema is in place, every
|
* columns) is defined only on Sequelize models — no separate "table alteration" scripts are
|
||||||
* versioned migration file under `scripts/migrations/` is automatically
|
* required after a fresh `migrate` + `seed:all` (see package.json `setup:fresh`).
|
||||||
* 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.');
|
||||||
|
|
||||||
// force: true drops existing tables — schema is rebuilt exactly from
|
// Synchronize models (force: true drops existing tables)
|
||||||
// Sequelize models, so every enum / column / index matches code.
|
// This ensures that the schema exactly matches the Sequelize models
|
||||||
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('----------------------------------------------------\n');
|
console.log('----------------------------------------------------');
|
||||||
|
|
||||||
if (!skipBaseline) {
|
|
||||||
await baselineMigrationsTable();
|
|
||||||
} else {
|
|
||||||
console.log('Skipping migration baseline (--no-baseline).');
|
|
||||||
}
|
|
||||||
|
|
||||||
process.exit(0);
|
process.exit(0);
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
|
|||||||
63
scripts/migrate_user_columns.ts
Normal file
63
scripts/migrate_user_columns.ts
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
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();
|
||||||
@ -1,52 +0,0 @@
|
|||||||
/**
|
|
||||||
* 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;
|
|
||||||
@ -1,58 +0,0 @@
|
|||||||
# 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.
|
|
||||||
@ -1,42 +0,0 @@
|
|||||||
/**
|
|
||||||
* 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;
|
|
||||||
36
scripts/remove_abhishek_app.ts
Normal file
36
scripts/remove_abhishek_app.ts
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
|
||||||
|
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();
|
||||||
@ -1,181 +0,0 @@
|
|||||||
/**
|
|
||||||
* 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();
|
|
||||||
21
scripts/test-areas.ts
Normal file
21
scripts/test-areas.ts
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
|
||||||
|
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();
|
||||||
34
scripts/test-regions.ts
Normal file
34
scripts/test-regions.ts
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
|
||||||
|
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();
|
||||||
26
scripts/test_enum_cast.ts
Normal file
26
scripts/test_enum_cast.ts
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
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();
|
||||||
22
scripts/test_insert.ts
Normal file
22
scripts/test_insert.ts
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
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();
|
||||||
42
scripts/update-enum.ts
Normal file
42
scripts/update-enum.ts
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
|
||||||
|
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();
|
||||||
46
scripts/update_dealer_codes_table.ts
Normal file
46
scripts/update_dealer_codes_table.ts
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
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();
|
||||||
33
scripts/update_enum.ts
Normal file
33
scripts/update_enum.ts
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
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();
|
||||||
26
scripts/update_participant_enum.ts
Normal file
26
scripts/update_participant_enum.ts
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
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();
|
||||||
47
scripts/verify-offboarding-status.ts
Normal file
47
scripts/verify-offboarding-status.ts
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
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.');
|
||||||
58
scripts/verify-standardized-offboarding.ts
Normal file
58
scripts/verify-standardized-offboarding.ts
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
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.');
|
||||||
@ -108,9 +108,6 @@ 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];
|
||||||
|
|
||||||
@ -236,9 +233,6 @@ 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) {
|
||||||
|
|||||||
@ -1,73 +0,0 @@
|
|||||||
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;
|
|
||||||
};
|
|
||||||
@ -784,12 +784,23 @@ export const takeAction = async (req: AuthRequest, res: Response) => {
|
|||||||
newCurrentStage = RELOCATION_STAGES.REJECTED;
|
newCurrentStage = RELOCATION_STAGES.REJECTED;
|
||||||
}
|
}
|
||||||
|
|
||||||
// SRS §12.2.8 — document upload/verification is tracked for readiness display
|
// SRS §12.2.8 — enforce mandatory document submission + verification before late-stage approvals
|
||||||
// (see getRelocationDocumentReadiness + progress calculation) but is NOT a hard
|
if (
|
||||||
// blocker on NBH Approval / Legal Clearance. Per business decision, these are
|
normalizedAction === 'APPROVE' &&
|
||||||
// senior approval authorities who retain discretion to proceed even when the
|
(
|
||||||
// readiness panel still shows missing uploads or pending verifications. The
|
request.currentStage === RELOCATION_STAGES.NBH_APPROVAL ||
|
||||||
// readiness data continues to be surfaced on the UI so gaps remain visible.
|
request.currentStage === RELOCATION_STAGES.LEGAL_CLEARANCE
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
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') {
|
||||||
|
|||||||
@ -59,69 +59,25 @@ async function apiRequest(endpoint, method = 'GET', body = null, token = null) {
|
|||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function login(email, password = PASSWORD) {
|
async function login(email) {
|
||||||
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 });
|
const data = await apiRequest('/auth/login', 'POST', { email, password: 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.');
|
||||||
|
}
|
||||||
|
|
||||||
const adminTokenEarly = await login(EMAILS.DD_ADMIN);
|
console.log(`[STEP 0] Logging in as Dealer: ${EMAILS.DEALER}...`);
|
||||||
const discovered = await discoverDealer(adminTokenEarly, EMAILS.DEALER);
|
const dealerToken = await login(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) {
|
||||||
@ -139,7 +95,19 @@ async function run() {
|
|||||||
console.log(`[STEP 1] Resuming request: ${requestId}`);
|
console.log(`[STEP 1] Resuming request: ${requestId}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const adminToken = adminTokenEarly;
|
// Sequence of users taking actions to advance stages
|
||||||
|
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;
|
||||||
@ -147,44 +115,15 @@ 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));
|
||||||
|
|
||||||
// Stage → actor mapping mirrors constitutional.controller.ts stageRoleMap.
|
let currentStep = 2 + startIndex;
|
||||||
// Driving the loop off the live currentStage (not a pre-computed index) keeps
|
for (let i = startIndex; i < approvalSequence.length; i++) {
|
||||||
// the script self-healing if the workflow shape ever changes.
|
const actor = approvalSequence[i];
|
||||||
const ACTOR_BY_STAGE = {
|
console.log(`[STEP ${currentStep}] ${actor.name} (${actor.email}) processing approval...`);
|
||||||
'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',
|
||||||
@ -197,7 +136,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 {
|
||||||
|
|||||||
@ -4,7 +4,6 @@ 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';
|
||||||
|
|||||||
@ -312,23 +312,18 @@ 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(
|
||||||
// Backend (termination.controller.ts) requires at least one .ppt/.pptx
|
'/termination',
|
||||||
// file for non-Super-Admin initiators, sent as multipart "files".
|
'POST',
|
||||||
const form = new FormData();
|
{
|
||||||
form.append('dealerId', targetDealer.id);
|
dealerId: targetDealer.id,
|
||||||
form.append('category', args.category || 'Performance');
|
category: args.category || 'Performance',
|
||||||
form.append('reason', args.reason || 'Consistently failed to meet commitment targets.');
|
reason: args.reason || 'Consistently failed to meet commitment targets.',
|
||||||
form.append('proposedLwd', new Date().toISOString().split('T')[0]);
|
proposedLwd: new Date().toISOString().split('T')[0],
|
||||||
form.append('comments', 'E2E termination — follows UI stage order (no stacked partial approvals).');
|
comments: 'E2E termination — follows UI stage order (no stacked partial approvals).'
|
||||||
|
},
|
||||||
const dummyPptxBytes = Buffer.from('E2E placeholder presentation');
|
asmToken
|
||||||
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 {
|
||||||
|
|||||||
@ -10,7 +10,6 @@ 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);
|
||||||
@ -382,205 +381,204 @@ 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).
|
||||||
// Route is POST /loi/request/:requestId/generate — requestId goes in the URL, not the body.
|
// log(9, 'Generating final LOI document (bridges to Dealer Code Generation)...');
|
||||||
log(9, 'Generating final LOI document (bridges to Dealer Code Generation)...');
|
// await apiRequest('/loi/generate-document', 'POST', { requestId: loiRequestId }, adminToken);
|
||||||
await apiRequest(`/loi/request/${loiRequestId}/generate`, 'POST', {}, adminToken);
|
// await delay();
|
||||||
await delay();
|
// statusBeforeCodeGen = await getApplicationStatus(applicationUUID, adminToken);
|
||||||
statusBeforeCodeGen = await getApplicationStatus(applicationUUID, adminToken);
|
// log(9, `Status after LOI generateDocument: ${statusBeforeCodeGen}`);
|
||||||
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.`);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user