database script files cleaned migration table created planned for fresh setup
This commit is contained in:
parent
80495a78a6
commit
41fe7963a7
6
build/assets/index-CDNp5hMY.css
Normal file
6
build/assets/index-CDNp5hMY.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
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" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Royal Enfield Onboarding</title>
|
||||
<script type="module" crossorigin src="/assets/index-XdyJ-8da.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-DqVo88us.css">
|
||||
<script type="module" crossorigin src="/assets/index-CIW1_Mz_.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-CDNp5hMY.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
||||
@ -41,7 +41,7 @@ Ordered by impact. Update this file when items ship.
|
||||
|
||||
## Verification checklist
|
||||
|
||||
1. `npx tsx scripts/migrate-sla-tracking-schema.ts` (once per DB if needed)
|
||||
1. `npm run migrate:up` (applies pending versioned migrations, including SLA schema)
|
||||
2. `npx tsx scripts/seed-sla-configs.ts`
|
||||
3. `ENABLE_REDIS=true` + restart API
|
||||
4. Operations monitor → analytics cards, **My queue**, **Export CSV**, **Schedulers** → questionnaire settings
|
||||
|
||||
@ -38,19 +38,20 @@ Or use **Master → SLA Configuration → Initialize defaults** in the UI.
|
||||
|
||||
## DB note
|
||||
|
||||
If `sla_tracking.metadata` (or `entityType` / `entityId`) is missing on an older database, run:
|
||||
Schema for `sla_tracking` and `sla_notification_dispatches` is defined in the
|
||||
Sequelize models (`src/database/models/compliance/`). On a fresh database
|
||||
`npm run migrate` builds them automatically.
|
||||
|
||||
For environments that already hold data, run the versioned migration runner
|
||||
which only applies the migrations not yet recorded in the `migrations` table:
|
||||
|
||||
```bash
|
||||
npx tsx scripts/migrate-sla-tracking-schema.ts
|
||||
npm run migrate:up
|
||||
```
|
||||
|
||||
### 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.
|
||||
|
||||
```bash
|
||||
npx tsx scripts/migrate-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. Created by the model definition + the migration `scripts/migrations/20260526000000_create_sla_notification_dispatches.ts`.
|
||||
|
||||
| `dispatchType` | `thresholdKey` example | Sends |
|
||||
|----------------|------------------------|--------|
|
||||
|
||||
@ -10,7 +10,10 @@
|
||||
"build": "tsc",
|
||||
"type-check": "tsc --noEmit",
|
||||
"migrate": "tsx scripts/migrate.ts",
|
||||
"migrate:sla-dispatches": "tsx scripts/migrate-sla-notification-dispatches.ts",
|
||||
"migrate:up": "tsx scripts/run-migrations.ts",
|
||||
"migrate:status": "tsx scripts/run-migrations.ts --status",
|
||||
"migrate:baseline": "tsx scripts/run-migrations.ts --baseline",
|
||||
"migrate:create": "tsx scripts/create-migration.ts",
|
||||
"reset:stable": "tsx scripts/reset_db_stable.ts",
|
||||
"seed": "tsx scripts/seed_normalized_data.ts",
|
||||
"seed:roles": "tsx scripts/seed-roles.ts",
|
||||
|
||||
@ -1,39 +0,0 @@
|
||||
import 'dotenv/config';
|
||||
import db from '../src/database/models/index.js';
|
||||
const { Role } = db;
|
||||
|
||||
async function addArchitectureRole() {
|
||||
console.log('Adding Architecture Role...');
|
||||
try {
|
||||
await db.sequelize.authenticate();
|
||||
|
||||
await Role.findOrCreate({
|
||||
where: { roleCode: 'ARCHITECTURE' },
|
||||
defaults: {
|
||||
roleCode: 'ARCHITECTURE',
|
||||
roleName: 'Architecture',
|
||||
category: 'DEPARTMENT',
|
||||
isActive: true
|
||||
}
|
||||
});
|
||||
|
||||
// Also add the 'Architecture' alias if needed for frontend mapping
|
||||
await Role.findOrCreate({
|
||||
where: { roleCode: 'Architecture' },
|
||||
defaults: {
|
||||
roleCode: 'Architecture',
|
||||
roleName: 'Architecture Team',
|
||||
category: 'DEPARTMENT',
|
||||
isActive: true
|
||||
}
|
||||
});
|
||||
|
||||
console.log('✅ Architecture role added successfully!');
|
||||
process.exit(0);
|
||||
} catch (error) {
|
||||
console.error('❌ Failed to add role:', error);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
addArchitectureRole();
|
||||
@ -1,26 +0,0 @@
|
||||
|
||||
import { Sequelize } from 'sequelize';
|
||||
|
||||
const sequelize = new Sequelize('royal_enfield_onboarding', 'laxman', '<.efvP1D0^80Z)r5', {
|
||||
host: 'localhost',
|
||||
dialect: 'postgres',
|
||||
logging: console.log
|
||||
});
|
||||
|
||||
const run = async () => {
|
||||
try {
|
||||
await sequelize.authenticate();
|
||||
console.log('Connected to database.');
|
||||
|
||||
console.log('Adding decision column to interview_evaluations table...');
|
||||
await sequelize.query('ALTER TABLE "interview_evaluations" ADD COLUMN IF NOT EXISTS "decision" VARCHAR(255);');
|
||||
|
||||
console.log('Column added successfully.');
|
||||
process.exit(0);
|
||||
} catch (error) {
|
||||
console.error('Error:', error);
|
||||
process.exit(1);
|
||||
}
|
||||
};
|
||||
|
||||
run();
|
||||
@ -1,28 +0,0 @@
|
||||
|
||||
import 'dotenv/config';
|
||||
import db from '../src/database/models/index.js';
|
||||
|
||||
const updateEnum = async () => {
|
||||
try {
|
||||
console.log('>>> STARTING ENUM MIGRATION (Level 3) <<<');
|
||||
await db.sequelize.authenticate();
|
||||
console.log('Database connection established.');
|
||||
|
||||
try {
|
||||
await db.sequelize.query(`ALTER TYPE "enum_applications_overallStatus" ADD VALUE IF NOT EXISTS 'Level 3 Approved';`);
|
||||
console.log('Added Level 3 Approved');
|
||||
} catch (e) {
|
||||
console.log('Level 3 Approved likely exists or error', e instanceof Error ? e.message : String(e));
|
||||
}
|
||||
|
||||
console.log('>>> SUCCESS: Enum values updated <<<');
|
||||
|
||||
await db.sequelize.close();
|
||||
process.exit(0);
|
||||
} catch (error) {
|
||||
console.error('>>> ERROR: Failed to update Enum', error);
|
||||
process.exit(1);
|
||||
}
|
||||
};
|
||||
|
||||
updateEnum();
|
||||
@ -1,27 +0,0 @@
|
||||
import 'dotenv/config';
|
||||
import db from '../src/database/models/index.js';
|
||||
|
||||
const addRecoveryEnum = async () => {
|
||||
try {
|
||||
console.log('>>> STARTING ENUM UPDATE: FnFLineItem itemType <<<');
|
||||
await db.sequelize.authenticate();
|
||||
console.log('Database connection established.');
|
||||
|
||||
// Raw query to add 'Recovery' to the itemType enum
|
||||
try {
|
||||
await db.sequelize.query(`ALTER TYPE "enum_fnf_line_items_itemType" ADD VALUE IF NOT EXISTS 'Recovery';`);
|
||||
console.log('SUCCESS: Added "Recovery" to "enum_fnf_line_items_itemType"');
|
||||
} catch (e) {
|
||||
console.log('NOTICE: "Recovery" likely already exists or another error occurred:', e.message);
|
||||
}
|
||||
|
||||
await db.sequelize.close();
|
||||
console.log('>>> ENUM UPDATE COMPLETED <<<');
|
||||
process.exit(0);
|
||||
} catch (error) {
|
||||
console.error('>>> ERROR: Failed to update Enum:', error);
|
||||
process.exit(1);
|
||||
}
|
||||
};
|
||||
|
||||
addRecoveryEnum();
|
||||
@ -1,74 +0,0 @@
|
||||
|
||||
import dotenv from 'dotenv';
|
||||
import path from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
dotenv.config({ path: path.join(__dirname, '../.env') });
|
||||
|
||||
import db from '../src/database/models/index.js';
|
||||
import { syncLocationManagers } from '../src/modules/master/syncHierarchy.service.js';
|
||||
|
||||
async function run() {
|
||||
try {
|
||||
// 1. Find the South Delhi district
|
||||
const district = await db.District.findOne({
|
||||
where: { name: { [db.Sequelize.Op.iLike]: '%South Delhi%' } }
|
||||
});
|
||||
|
||||
if (!district) {
|
||||
console.log('District "South Delhi" not found');
|
||||
return;
|
||||
}
|
||||
console.log(`Found District: ${district.name} (${district.id})`);
|
||||
|
||||
// 2. Find a DD-AM user
|
||||
// The role code might be 'DD AM' or 'DD-AM' based on constants
|
||||
const user = await db.User.findOne({
|
||||
where: {
|
||||
[db.Sequelize.Op.or]: [
|
||||
{ roleCode: 'DD AM' },
|
||||
{ roleCode: 'DD-AM' }
|
||||
],
|
||||
isActive: true
|
||||
}
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
console.log('No active DD-AM user found');
|
||||
return;
|
||||
}
|
||||
console.log(`Found DD-AM User: ${user.fullName} (${user.id})`);
|
||||
|
||||
// 3. Create/Update UserRole mapping
|
||||
const [userRole, created] = await db.UserRole.findOrCreate({
|
||||
where: {
|
||||
userId: user.id,
|
||||
districtId: district.id,
|
||||
isActive: true
|
||||
},
|
||||
defaults: {
|
||||
roleId: (await db.Role.findOne({ where: { roleCode: user.roleCode } })).id,
|
||||
isPrimary: true,
|
||||
isActive: true
|
||||
}
|
||||
});
|
||||
|
||||
if (created) {
|
||||
console.log('Created new UserRole assignment');
|
||||
} else {
|
||||
console.log('UserRole assignment already exists');
|
||||
}
|
||||
|
||||
// 4. Sync Location Managers
|
||||
await syncLocationManagers(district.id);
|
||||
console.log('Sync completed');
|
||||
|
||||
process.exit(0);
|
||||
} catch (error) {
|
||||
console.error('Error:', error);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
run();
|
||||
@ -1,28 +0,0 @@
|
||||
import 'dotenv/config';
|
||||
import db from '../src/database/models/index.js';
|
||||
|
||||
async function main() {
|
||||
const { sequelize, SLANotificationDispatch, SLATracking } = db;
|
||||
await sequelize.authenticate();
|
||||
const count = await SLANotificationDispatch.count();
|
||||
const activeTracks = await SLATracking.count({ where: { isActive: true, endTime: null } });
|
||||
const recent = await SLANotificationDispatch.findAll({
|
||||
limit: 15,
|
||||
order: [['sentAt', 'DESC']]
|
||||
});
|
||||
console.log('dispatches:', count, 'active tracks:', activeTracks);
|
||||
for (const r of recent) {
|
||||
console.log(
|
||||
r.dispatchType,
|
||||
r.thresholdKey,
|
||||
String(r.trackingId).slice(0, 8),
|
||||
r.sentAt
|
||||
);
|
||||
}
|
||||
await sequelize.close();
|
||||
}
|
||||
|
||||
main().catch((e) => {
|
||||
console.error(e);
|
||||
process.exit(1);
|
||||
});
|
||||
@ -1,18 +0,0 @@
|
||||
import 'dotenv/config';
|
||||
import { initializeSmtpConfig } from '../src/services/smtpConfig.service.js';
|
||||
|
||||
async function main() {
|
||||
const cfg = await initializeSmtpConfig();
|
||||
console.log('enabled:', cfg.enabled);
|
||||
console.log('source:', cfg.source);
|
||||
console.log('host:', cfg.host);
|
||||
console.log('port:', cfg.port);
|
||||
console.log('user:', cfg.auth.user || '(empty)');
|
||||
console.log('pass:', cfg.auth.pass ? `*** (${cfg.auth.pass.length})` : '(empty)');
|
||||
console.log('from:', cfg.from);
|
||||
}
|
||||
|
||||
main().catch((e) => {
|
||||
console.error(e);
|
||||
process.exit(1);
|
||||
});
|
||||
@ -1,26 +0,0 @@
|
||||
import db from '../src/database/models/index.js';
|
||||
|
||||
async function check() {
|
||||
try {
|
||||
const app = await (db as any).Application.findOne({
|
||||
where: { email: 'test-dealer-tumkur@example.com' },
|
||||
include: [{ model: (db as any).District, as: 'district' }]
|
||||
});
|
||||
|
||||
if (app) {
|
||||
console.log('Application Found:');
|
||||
console.log('ID:', app.applicationId);
|
||||
console.log('District Name:', app.district ? app.district.name : 'NULL');
|
||||
console.log('District ID:', app.districtId);
|
||||
console.log('Is Opportunity Available (Status):', app.overallStatus);
|
||||
} else {
|
||||
console.log('Application not found.');
|
||||
}
|
||||
process.exit(0);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
check();
|
||||
@ -1,18 +0,0 @@
|
||||
import db from '../src/database/models/index.js';
|
||||
|
||||
async function checkColumn() {
|
||||
try {
|
||||
const [results]: any = await db.sequelize.query(`
|
||||
SELECT column_name, data_type, udt_name
|
||||
FROM information_schema.columns
|
||||
WHERE table_name = 'request_participants' AND column_name = 'participantType'
|
||||
`);
|
||||
console.log('Column definition:', results[0]);
|
||||
} catch (error: any) {
|
||||
console.error('Error fetching column:', error.message);
|
||||
} finally {
|
||||
process.exit(0);
|
||||
}
|
||||
}
|
||||
|
||||
checkColumn();
|
||||
@ -1,19 +0,0 @@
|
||||
import db from '../src/database/models/index.js';
|
||||
|
||||
async function checkEnum() {
|
||||
try {
|
||||
const [results]: any = await db.sequelize.query(`
|
||||
SELECT enumlabel
|
||||
FROM pg_enum
|
||||
JOIN pg_type ON pg_enum.enumtypid = pg_type.oid
|
||||
WHERE typname = 'enum_request_participants_participantType'
|
||||
`);
|
||||
console.log('Current enum values:', results.map((r: any) => r.enumlabel).join(', '));
|
||||
} catch (error: any) {
|
||||
console.error('Error fetching enum:', error.message);
|
||||
} finally {
|
||||
process.exit(0);
|
||||
}
|
||||
}
|
||||
|
||||
checkEnum();
|
||||
@ -1,48 +0,0 @@
|
||||
|
||||
import dotenv from 'dotenv';
|
||||
import path from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
dotenv.config({ path: path.join(__dirname, '../.env') });
|
||||
|
||||
import db from '../src/database/models/index.js';
|
||||
|
||||
async function run() {
|
||||
try {
|
||||
const app = await db.Application.findOne({
|
||||
order: [['updatedAt', 'DESC']],
|
||||
include: [
|
||||
{ model: db.District, as: 'district' },
|
||||
{
|
||||
model: db.RequestParticipant,
|
||||
as: 'participants',
|
||||
include: [{ model: db.User, as: 'user' }]
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
if (!app) {
|
||||
console.log('No applications found');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('Application ID:', app.id);
|
||||
console.log('Status:', app.status);
|
||||
console.log('District:', app.district?.name);
|
||||
console.log('District ddAmId:', app.district?.ddAmId);
|
||||
console.log('District asmId:', app.district?.asmId);
|
||||
|
||||
console.log('Participants:');
|
||||
app.participants?.forEach(p => {
|
||||
console.log(`- ${p.user?.fullName} (${p.metadata?.role})`);
|
||||
});
|
||||
|
||||
process.exit(0);
|
||||
} catch (error) {
|
||||
console.error('Error:', error);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
run();
|
||||
@ -1,26 +0,0 @@
|
||||
import db from '../src/database/models/index.js';
|
||||
|
||||
async function cleanup() {
|
||||
try {
|
||||
await db.sequelize.authenticate();
|
||||
console.log('Database connected.');
|
||||
|
||||
const [results1]: any = await db.sequelize.query(`
|
||||
DELETE FROM interview_participants
|
||||
WHERE "interviewId" NOT IN (SELECT id FROM interviews)
|
||||
`);
|
||||
|
||||
const [results2]: any = await db.sequelize.query(`
|
||||
DELETE FROM interview_evaluations
|
||||
WHERE "interviewId" NOT IN (SELECT id FROM interviews)
|
||||
`);
|
||||
|
||||
console.log('Cleanup finished.');
|
||||
process.exit(0);
|
||||
} catch (err) {
|
||||
console.error('Cleanup failed:', err);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
cleanup();
|
||||
106
scripts/create-migration.ts
Normal file
106
scripts/create-migration.ts
Normal file
@ -0,0 +1,106 @@
|
||||
/**
|
||||
* Scaffold a new migration file under `scripts/migrations/`.
|
||||
*
|
||||
* Usage: npm run migrate:create -- <snake_case_description>
|
||||
* e.g.: npm run migrate:create -- add_finance_kyc_column
|
||||
*
|
||||
* The file is named `<YYYYMMDDHHMMSS>_<description>.ts` (UTC timestamp).
|
||||
* Author then edits `up()` to implement the schema change.
|
||||
*/
|
||||
|
||||
import { promises as fs } from 'fs';
|
||||
import path from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
|
||||
const SCRIPTS_DIR = path.dirname(fileURLToPath(import.meta.url));
|
||||
const MIGRATIONS_DIR = path.join(SCRIPTS_DIR, 'migrations');
|
||||
|
||||
function pad(n: number, width = 2): string {
|
||||
return String(n).padStart(width, '0');
|
||||
}
|
||||
|
||||
function utcTimestamp(): string {
|
||||
const d = new Date();
|
||||
return (
|
||||
d.getUTCFullYear().toString() +
|
||||
pad(d.getUTCMonth() + 1) +
|
||||
pad(d.getUTCDate()) +
|
||||
pad(d.getUTCHours()) +
|
||||
pad(d.getUTCMinutes()) +
|
||||
pad(d.getUTCSeconds())
|
||||
);
|
||||
}
|
||||
|
||||
function sanitizeName(input: string): string {
|
||||
const cleaned = input
|
||||
.trim()
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9]+/g, '_')
|
||||
.replace(/^_+|_+$/g, '');
|
||||
if (!cleaned) {
|
||||
throw new Error('Migration name is empty after sanitisation.');
|
||||
}
|
||||
if (cleaned.length > 80) {
|
||||
throw new Error(
|
||||
`Migration name "${cleaned}" is too long (${cleaned.length}). Keep it under 80 chars.`
|
||||
);
|
||||
}
|
||||
return cleaned;
|
||||
}
|
||||
|
||||
async function main(): Promise<void> {
|
||||
const rawName = process.argv.slice(2).join('_');
|
||||
if (!rawName) {
|
||||
console.error('Missing migration name.');
|
||||
console.error('Usage: npm run migrate:create -- <snake_case_description>');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const name = sanitizeName(rawName);
|
||||
const ts = utcTimestamp();
|
||||
const filename = `${ts}_${name}.ts`;
|
||||
const target = path.join(MIGRATIONS_DIR, filename);
|
||||
|
||||
const body = `/**
|
||||
* Migration: ${name.replace(/_/g, ' ')}
|
||||
*
|
||||
* Generated at ${new Date().toISOString()}.
|
||||
* Implement up() with idempotent DDL where possible.
|
||||
*/
|
||||
|
||||
import type { QueryInterface, Sequelize, Transaction } from 'sequelize';
|
||||
|
||||
export interface MigrationContext {
|
||||
queryInterface: QueryInterface;
|
||||
sequelize: Sequelize;
|
||||
transaction: Transaction;
|
||||
}
|
||||
|
||||
const migration = {
|
||||
async up({ sequelize, transaction }: MigrationContext): Promise<void> {
|
||||
// TODO: implement the schema change.
|
||||
// Example:
|
||||
// await sequelize.query(\`
|
||||
// ALTER TABLE "applications"
|
||||
// ADD COLUMN IF NOT EXISTS "kycReviewedAt" TIMESTAMPTZ NULL;
|
||||
// \`, { transaction });
|
||||
throw new Error('Migration ${filename} has no up() implementation yet.');
|
||||
}
|
||||
};
|
||||
|
||||
export default migration;
|
||||
`;
|
||||
|
||||
await fs.mkdir(MIGRATIONS_DIR, { recursive: true });
|
||||
await fs.writeFile(target, body, { flag: 'wx' });
|
||||
console.log(`Created ${path.relative(process.cwd(), target)}`);
|
||||
console.log('Next:');
|
||||
console.log(' 1. Edit the file and implement up().');
|
||||
console.log(' 2. Update the matching Sequelize model.');
|
||||
console.log(' 3. Run: npm run migrate:up');
|
||||
}
|
||||
|
||||
main().catch((err) => {
|
||||
console.error('create-migration failed:', err?.message || err);
|
||||
process.exit(1);
|
||||
});
|
||||
@ -1,40 +0,0 @@
|
||||
/**
|
||||
* Create System Audit Log Table
|
||||
*
|
||||
* Bootstraps the new `system_audit_logs` table on environments where the
|
||||
* full `migrate.ts` (sequelize.sync({ force: true })) cannot be run because
|
||||
* the database already holds production / shared data.
|
||||
*
|
||||
* Safe to re-run: uses `SystemAuditLog.sync()` (no `force`, no `alter`),
|
||||
* which is a no-op once the table exists.
|
||||
*
|
||||
* Run: npx tsx scripts/create-system-audit-log-table.ts
|
||||
*/
|
||||
|
||||
import 'dotenv/config';
|
||||
import db from '../src/database/models/index.js';
|
||||
|
||||
async function run() {
|
||||
console.log('🔄 Ensuring system_audit_logs table exists...');
|
||||
try {
|
||||
await db.sequelize.authenticate();
|
||||
console.log('📡 Database connection OK');
|
||||
|
||||
await db.SystemAuditLog.sync();
|
||||
|
||||
const [rows] = await db.sequelize.query(
|
||||
`SELECT COUNT(*)::int AS total FROM system_audit_logs`
|
||||
);
|
||||
const total = (rows as any[])[0]?.total ?? 0;
|
||||
|
||||
console.log('✅ system_audit_logs is ready');
|
||||
console.log(` Existing rows: ${total}`);
|
||||
process.exit(0);
|
||||
} catch (err: any) {
|
||||
console.error('❌ Failed to ensure system_audit_logs table:', err.message || err);
|
||||
if (err.stack) console.error(err.stack);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
run();
|
||||
@ -1,63 +0,0 @@
|
||||
|
||||
import db from '../src/database/models/index.js';
|
||||
|
||||
async function checkAreaManager() {
|
||||
try {
|
||||
console.log('Connecting to database...');
|
||||
await db.sequelize.authenticate();
|
||||
console.log('Database connected.');
|
||||
|
||||
// Fetch all areas
|
||||
const areas = await db.Area.findAll({
|
||||
include: [
|
||||
{ model: db.User, as: 'manager', attributes: ['id', 'fullName'] }
|
||||
]
|
||||
});
|
||||
|
||||
console.log(`Found ${areas.length} areas.`);
|
||||
|
||||
if (areas.length > 0) {
|
||||
areas.forEach((area: any) => {
|
||||
console.log(`Area: ${area.areaName} (${area.id})`);
|
||||
console.log(` - Manager ID (Field): ${area.managerId}`);
|
||||
console.log(` - Manager (Association): ${area.manager ? area.manager.fullName : 'None'}`);
|
||||
console.log('-----------------------------------');
|
||||
});
|
||||
|
||||
// Pick the first area and try to update it manually if managerId is null
|
||||
const targetArea = areas[0];
|
||||
// Find a user to assign (any user)
|
||||
const user = await db.User.findOne();
|
||||
|
||||
if (user) {
|
||||
console.log(`Attempting to assign User ${user.fullName} (${user.id}) to Area ${targetArea.areaName}...`);
|
||||
|
||||
targetArea.managerId = user.id;
|
||||
await targetArea.save();
|
||||
|
||||
console.log('Update saved. Re-fetching to verify...');
|
||||
|
||||
const updatedArea = await db.Area.findByPk(targetArea.id);
|
||||
console.log(`Re-fetched Area Manager ID: ${updatedArea?.managerId}`);
|
||||
|
||||
if (updatedArea?.managerId === user.id) {
|
||||
console.log('SUCCESS: Manager ID persisted correctly.');
|
||||
} else {
|
||||
console.error('FAILURE: Manager ID did not persist.');
|
||||
}
|
||||
} else {
|
||||
console.log('No users found to test assignment.');
|
||||
}
|
||||
|
||||
} else {
|
||||
console.log('No areas found.');
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error:', error);
|
||||
} finally {
|
||||
await db.sequelize.close();
|
||||
}
|
||||
}
|
||||
|
||||
checkAreaManager();
|
||||
@ -1,34 +0,0 @@
|
||||
|
||||
import { Sequelize } from 'sequelize';
|
||||
|
||||
const sequelize = new Sequelize('royal_enfield_onboarding', 'laxman', '<.efvP1D0^80Z)r5', {
|
||||
host: 'localhost',
|
||||
dialect: 'postgres',
|
||||
logging: false
|
||||
});
|
||||
|
||||
const run = async () => {
|
||||
try {
|
||||
await sequelize.authenticate();
|
||||
console.log('Connected to database.');
|
||||
|
||||
const [results] = await sequelize.query(`
|
||||
SELECT column_name, data_type
|
||||
FROM information_schema.columns
|
||||
WHERE table_name = 'interview_evaluations';
|
||||
`);
|
||||
console.log('Columns in interview_evaluations:');
|
||||
console.table(results);
|
||||
|
||||
const [evals] = await sequelize.query('SELECT * FROM "interview_evaluations" ORDER BY "createdAt" DESC LIMIT 1;');
|
||||
console.log('Latest evaluation:');
|
||||
console.log(evals[0]);
|
||||
|
||||
process.exit(0);
|
||||
} catch (error) {
|
||||
console.error('Error:', error);
|
||||
process.exit(1);
|
||||
}
|
||||
};
|
||||
|
||||
run();
|
||||
@ -1,20 +0,0 @@
|
||||
import db from '../src/database/models/index.js';
|
||||
|
||||
async function check() {
|
||||
try {
|
||||
const roles = await (db as any).Role.findAll();
|
||||
console.log('--- ROLES START ---');
|
||||
console.log(JSON.stringify(roles.map((r: any) => ({
|
||||
name: r.roleName,
|
||||
code: r.roleCode,
|
||||
id: r.id
|
||||
})), null, 2));
|
||||
console.log('--- ROLES END ---');
|
||||
process.exit(0);
|
||||
} catch (error) {
|
||||
console.error('Error listing roles:', error);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
check();
|
||||
@ -1,53 +0,0 @@
|
||||
/**
|
||||
* Script to delete a test relocation request by requestId
|
||||
* Usage: npx tsx scripts/delete-test-relocation.ts REL-1775129490244-5B9C
|
||||
*/
|
||||
import db from '../src/database/models/index.js';
|
||||
|
||||
async function deleteRelocationRequest(requestId: string) {
|
||||
try {
|
||||
console.log(`Deleting relocation request: ${requestId}`);
|
||||
|
||||
// Find the request
|
||||
const request = await db.RelocationRequest.findOne({
|
||||
where: { requestId }
|
||||
});
|
||||
|
||||
if (!request) {
|
||||
console.log(`Request ${requestId} not found`);
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
// Delete associated RequestParticipants
|
||||
await db.RequestParticipant.destroy({
|
||||
where: { requestId: request.id, requestType: 'relocation' }
|
||||
});
|
||||
console.log('Deleted associated participants');
|
||||
|
||||
// Delete associated Worknotes
|
||||
await db.Worknote.destroy({
|
||||
where: { requestId: request.id, requestType: 'relocation' }
|
||||
});
|
||||
console.log('Deleted associated worknotes');
|
||||
|
||||
// Delete the request
|
||||
await request.destroy();
|
||||
console.log(`Deleted relocation request: ${requestId}`);
|
||||
|
||||
console.log('✅ Done!');
|
||||
process.exit(0);
|
||||
} catch (error) {
|
||||
console.error('❌ Error:', error);
|
||||
process.exit(1);
|
||||
} finally {
|
||||
await db.sequelize.close();
|
||||
}
|
||||
}
|
||||
|
||||
const requestId = process.argv[2];
|
||||
if (!requestId) {
|
||||
console.log('Usage: npx tsx scripts/delete-test-relocation.ts <requestId>');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
deleteRelocationRequest(requestId);
|
||||
@ -1,76 +0,0 @@
|
||||
|
||||
import dotenv from 'dotenv';
|
||||
import path from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
dotenv.config({ path: path.join(__dirname, '../.env') });
|
||||
|
||||
import db from '../src/database/models/index.js';
|
||||
|
||||
async function run() {
|
||||
try {
|
||||
console.log('Associations for User:');
|
||||
const userAssoc = db.User.associations;
|
||||
Object.keys(userAssoc).forEach(key => {
|
||||
console.log(`- ${key}: ${userAssoc[key].associationType} to ${userAssoc[key].target.name}`);
|
||||
});
|
||||
|
||||
console.log('\nTrying findAll with managedAsmDistricts...');
|
||||
await db.User.findAll({
|
||||
limit: 1,
|
||||
include: [{ model: db.District, as: 'managedAsmDistricts' }]
|
||||
});
|
||||
console.log('Success with managedAsmDistricts');
|
||||
|
||||
console.log('\nTrying findAll with managedAreaDistricts...');
|
||||
await db.User.findAll({
|
||||
limit: 1,
|
||||
include: [{ model: db.District, as: 'managedAreaDistricts' }]
|
||||
});
|
||||
console.log('Success with managedAreaDistricts');
|
||||
|
||||
console.log('\nTrying FULL query from getASMs...');
|
||||
await db.User.findAll({
|
||||
where: {
|
||||
roleCode: { [db.Sequelize.Op.in]: ['ASM', 'AREA SALES MANAGER', 'DD-AM', 'DD AM'] },
|
||||
isActive: true
|
||||
},
|
||||
include: [
|
||||
{
|
||||
model: db.UserRole,
|
||||
as: 'userRoles',
|
||||
where: { isActive: true },
|
||||
required: false,
|
||||
include: [{ model: db.Role, as: 'role', where: { roleCode: { [db.Sequelize.Op.in]: ['ASM', 'DD-AM', 'DD AM'] } } }]
|
||||
},
|
||||
{
|
||||
model: db.District,
|
||||
as: 'managedAsmDistricts',
|
||||
include: [
|
||||
{ model: db.State, as: 'state', attributes: ['id', 'name'] },
|
||||
{ model: db.Region, as: 'region', attributes: ['id', 'name'] },
|
||||
{ model: db.Zone, as: 'zone', attributes: ['id', 'name'] }
|
||||
]
|
||||
},
|
||||
{
|
||||
model: db.District,
|
||||
as: 'managedAreaDistricts',
|
||||
include: [
|
||||
{ model: db.State, as: 'state', attributes: ['id', 'name'] },
|
||||
{ model: db.Region, as: 'region', attributes: ['id', 'name'] },
|
||||
{ model: db.Zone, as: 'zone', attributes: ['id', 'name'] }
|
||||
]
|
||||
}
|
||||
],
|
||||
});
|
||||
console.log('Success with FULL query');
|
||||
|
||||
process.exit(0);
|
||||
} catch (error) {
|
||||
console.error('Error:', error);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
run();
|
||||
@ -1,24 +0,0 @@
|
||||
|
||||
import dotenv from 'dotenv';
|
||||
import path from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
dotenv.config({ path: path.join(__dirname, '../.env') });
|
||||
|
||||
import db from '../src/database/models/index.js';
|
||||
|
||||
async function run() {
|
||||
try {
|
||||
const u = await db.User.findOne({
|
||||
where: { fullName: { [db.Sequelize.Op.iLike]: '%abhishek%' } }
|
||||
});
|
||||
console.log(JSON.stringify(u, null, 2));
|
||||
process.exit(0);
|
||||
} catch (error) {
|
||||
console.error('Error:', error);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
run();
|
||||
@ -1,26 +0,0 @@
|
||||
|
||||
import { Sequelize } from 'sequelize';
|
||||
|
||||
const sequelize = new Sequelize('royal_enfield_onboarding', 'laxman', 'Admin@123', {
|
||||
host: 'localhost',
|
||||
dialect: 'postgres',
|
||||
logging: console.log
|
||||
});
|
||||
|
||||
const run = async () => {
|
||||
try {
|
||||
await sequelize.authenticate();
|
||||
console.log('Connected to database.');
|
||||
|
||||
console.log('Adding asmCode column to area_managers table...');
|
||||
await sequelize.query('ALTER TABLE "area_managers" ADD COLUMN IF NOT EXISTS "asmCode" VARCHAR(255);');
|
||||
|
||||
console.log('Column added successfully.');
|
||||
process.exit(0);
|
||||
} catch (error) {
|
||||
console.error('Error:', error);
|
||||
process.exit(1);
|
||||
}
|
||||
};
|
||||
|
||||
run();
|
||||
@ -1,26 +0,0 @@
|
||||
|
||||
import { Sequelize } from 'sequelize';
|
||||
|
||||
const sequelize = new Sequelize('royal_enfield_onboarding', 'laxman', '<.efvP1D0^80Z)r5', {
|
||||
host: 'localhost',
|
||||
dialect: 'postgres',
|
||||
logging: console.log
|
||||
});
|
||||
|
||||
const run = async () => {
|
||||
try {
|
||||
await sequelize.authenticate();
|
||||
console.log('Connected to database.');
|
||||
|
||||
console.log('Adding remarks column to interview_evaluations table...');
|
||||
await sequelize.query('ALTER TABLE "interview_evaluations" ADD COLUMN IF NOT EXISTS "remarks" TEXT;');
|
||||
|
||||
console.log('Column added successfully.');
|
||||
process.exit(0);
|
||||
} catch (error) {
|
||||
console.error('Error:', error);
|
||||
process.exit(1);
|
||||
}
|
||||
};
|
||||
|
||||
run();
|
||||
@ -1,47 +0,0 @@
|
||||
|
||||
import 'dotenv/config';
|
||||
import { Sequelize } from 'sequelize';
|
||||
|
||||
const sequelize = new Sequelize('royal_enfield_onboarding', 'laxman', '<.efvP1D0^80Z)r5', {
|
||||
host: 'localhost',
|
||||
dialect: 'postgres',
|
||||
logging: console.log
|
||||
});
|
||||
|
||||
const run = async () => {
|
||||
try {
|
||||
await sequelize.authenticate();
|
||||
console.log('Connected to database.');
|
||||
|
||||
const stagesToAdd = [
|
||||
'Level 1 Approved',
|
||||
'Level 2 Approved',
|
||||
'Level 2 Recommended',
|
||||
'Level 3 Approved'
|
||||
];
|
||||
|
||||
for (const stage of stagesToAdd) {
|
||||
try {
|
||||
await sequelize.query(`ALTER TYPE "enum_applications_currentStage" ADD VALUE IF NOT EXISTS '${stage}';`);
|
||||
console.log(`Added '${stage}' to enum_applications_currentStage`);
|
||||
} catch (e: any) {
|
||||
console.log(`'${stage}' might already exist in enum_applications_currentStage or error:`, e.message);
|
||||
}
|
||||
|
||||
try {
|
||||
await sequelize.query(`ALTER TYPE "enum_applications_overallStatus" ADD VALUE IF NOT EXISTS '${stage}';`);
|
||||
console.log(`Added '${stage}' to enum_applications_overallStatus`);
|
||||
} catch (e: any) {
|
||||
console.log(`'${stage}' might already exist in enum_applications_overallStatus or error:`, e.message);
|
||||
}
|
||||
}
|
||||
|
||||
console.log('Successfully updated enums.');
|
||||
process.exit(0);
|
||||
} catch (error) {
|
||||
console.error('Error:', error);
|
||||
process.exit(1);
|
||||
}
|
||||
};
|
||||
|
||||
run();
|
||||
@ -1,40 +0,0 @@
|
||||
|
||||
import dotenv from 'dotenv';
|
||||
import path from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
dotenv.config({ path: path.join(__dirname, '../.env') });
|
||||
|
||||
import db from '../src/database/models/index.js';
|
||||
|
||||
async function run() {
|
||||
try {
|
||||
const district = await db.District.findOne({
|
||||
where: { name: { [db.Sequelize.Op.iLike]: '%South Delhi%' } }
|
||||
});
|
||||
|
||||
if (!district) {
|
||||
console.log('South Delhi not found');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('Current assignment for South Delhi:');
|
||||
console.log(`ddAmId: ${district.ddAmId}`);
|
||||
console.log(`asmId: ${district.asmId}`);
|
||||
console.log(`zmId: ${district.zmId}`);
|
||||
|
||||
if (district.asmId) {
|
||||
console.log(`Removing ASM ${district.asmId} from South Delhi...`);
|
||||
await district.update({ asmId: null, asmCode: null });
|
||||
console.log('ASM removed.');
|
||||
}
|
||||
|
||||
process.exit(0);
|
||||
} catch (error) {
|
||||
console.error('Error:', error);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
run();
|
||||
@ -1,21 +0,0 @@
|
||||
|
||||
import db from '../src/database/models/index.ts';
|
||||
|
||||
const syncDb = async () => {
|
||||
try {
|
||||
console.log('Connecting to database...');
|
||||
await db.sequelize.authenticate();
|
||||
console.log('Database connected.');
|
||||
|
||||
console.log('Syncing database schema (alter: true)...');
|
||||
await db.sequelize.sync({ alter: true });
|
||||
console.log('Database synced successfully.');
|
||||
|
||||
process.exit(0);
|
||||
} catch (error) {
|
||||
console.error('Error syncing database:', error);
|
||||
process.exit(1);
|
||||
}
|
||||
};
|
||||
|
||||
syncDb();
|
||||
@ -1,29 +0,0 @@
|
||||
|
||||
import { Sequelize } from 'sequelize';
|
||||
|
||||
const sequelize = new Sequelize('royal_enfield_onboarding', 'laxman', '<.efvP1D0^80Z)r5', {
|
||||
host: 'localhost',
|
||||
dialect: 'postgres',
|
||||
logging: console.log
|
||||
});
|
||||
|
||||
const run = async () => {
|
||||
try {
|
||||
await sequelize.authenticate();
|
||||
console.log('Connected to database.');
|
||||
|
||||
console.log('Renaming recommendation to decision and remarks to decisionRemarks in interview_evaluations...');
|
||||
|
||||
// Use a transaction for safety
|
||||
await sequelize.query('ALTER TABLE "interview_evaluations" RENAME COLUMN "recommendation" TO "decision";');
|
||||
await sequelize.query('ALTER TABLE "interview_evaluations" RENAME COLUMN "remarks" TO "decisionRemarks";');
|
||||
|
||||
console.log('Columns renamed successfully.');
|
||||
process.exit(0);
|
||||
} catch (error) {
|
||||
console.error('Error during migration:', error);
|
||||
process.exit(1);
|
||||
}
|
||||
};
|
||||
|
||||
run();
|
||||
@ -1,83 +0,0 @@
|
||||
/**
|
||||
* Migration Script: Clean up onboarding_documents table.
|
||||
*
|
||||
* What it does (idempotent — safe to re-run):
|
||||
* 1. Drops legacy columns `requestId` and `requestType` (and their indexes).
|
||||
* These were generic catch-alls from when a single documents table routed
|
||||
* across modules. Each module now has its own dedicated documents table
|
||||
* (resignation_documents, termination_documents, constitutional_documents,
|
||||
* relocation_documents), so these columns are dead weight on
|
||||
* onboarding_documents and are not read or written anywhere in code.
|
||||
* 2. Adds two indexes the UI actually queries:
|
||||
* - (applicationId, stage) -> Progress / Documents tab grouping
|
||||
* - documentType -> EOR auto-link in onboarding.controller.ts
|
||||
*
|
||||
* What it does NOT do:
|
||||
* - No new "documentName" column. The user-entered document name is sent as
|
||||
* the FormData filename and stored in the existing `fileName` column.
|
||||
* - Does not touch `dealerId` (the Dealer <-> OnboardingDocument association
|
||||
* references it; it stays for future use).
|
||||
*
|
||||
* Run: npx tsx scripts/migrate-onboarding-documents-cleanup.ts
|
||||
*/
|
||||
import 'dotenv/config';
|
||||
import db from '../src/database/models/index.js';
|
||||
|
||||
const TABLE = 'onboarding_documents';
|
||||
|
||||
async function migrate() {
|
||||
const queryInterface = db.sequelize.getQueryInterface();
|
||||
|
||||
try {
|
||||
console.log(`🔄 Cleaning up ${TABLE} ...\n`);
|
||||
await db.sequelize.authenticate();
|
||||
|
||||
const tableInfo = await queryInterface.describeTable(TABLE);
|
||||
|
||||
// 1) Drop the index on requestId first (if it exists). Index name depends on
|
||||
// how Sequelize/Postgres generated it — try the common variants.
|
||||
for (const idxName of [
|
||||
`${TABLE}_requestId`,
|
||||
`${TABLE}_request_id`,
|
||||
]) {
|
||||
try {
|
||||
await db.sequelize.query(`DROP INDEX IF EXISTS "${idxName}"`);
|
||||
console.log(`✓ Dropped index ${idxName} (if existed)`);
|
||||
} catch (err: any) {
|
||||
console.log(`- Skipped index ${idxName}: ${err.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
// 2) Drop the unused columns (idempotent via describeTable check).
|
||||
for (const col of ['requestId', 'requestType']) {
|
||||
if (tableInfo[col]) {
|
||||
console.log(`Dropping column ${col} ...`);
|
||||
await queryInterface.removeColumn(TABLE, col);
|
||||
console.log(`✓ Dropped column ${col}`);
|
||||
} else {
|
||||
console.log(`- Column ${col} not present (already cleaned)`);
|
||||
}
|
||||
}
|
||||
|
||||
// 3) Add useful indexes (idempotent — Postgres IF NOT EXISTS).
|
||||
await db.sequelize.query(
|
||||
`CREATE INDEX IF NOT EXISTS "${TABLE}_applicationId_stage" ON ${TABLE} ("applicationId", "stage")`
|
||||
);
|
||||
console.log(`✓ Ensured index ${TABLE}_applicationId_stage`);
|
||||
|
||||
await db.sequelize.query(
|
||||
`CREATE INDEX IF NOT EXISTS "${TABLE}_documentType" ON ${TABLE} ("documentType")`
|
||||
);
|
||||
console.log(`✓ Ensured index ${TABLE}_documentType`);
|
||||
|
||||
console.log('\n✅ Migration completed successfully!');
|
||||
} catch (error: any) {
|
||||
console.error('\n❌ Migration failed:', error.message);
|
||||
if (error.stack) console.error('\nStack Trace:\n', error.stack);
|
||||
process.exit(1);
|
||||
} finally {
|
||||
await db.sequelize.close();
|
||||
}
|
||||
}
|
||||
|
||||
migrate();
|
||||
@ -1,74 +0,0 @@
|
||||
/**
|
||||
* Migration Script: Add newDistrictId and newStateId to RelocationRequest
|
||||
* Run: npx ts-node scripts/migrate-relocation-schema.ts
|
||||
*/
|
||||
import db from '../src/database/models/index.js';
|
||||
|
||||
async function migrate() {
|
||||
const queryInterface = db.sequelize.getQueryInterface();
|
||||
|
||||
try {
|
||||
console.log('Starting relocation schema migration...');
|
||||
|
||||
// Get table description to check existing columns
|
||||
const tableInfo = await queryInterface.describeTable('relocation_requests');
|
||||
|
||||
// Add newDistrictId column if not exists
|
||||
if (!tableInfo.newDistrictId) {
|
||||
console.log('Adding newDistrictId column...');
|
||||
await queryInterface.addColumn('relocation_requests', 'newDistrictId', {
|
||||
type: db.Sequelize.DataTypes.UUID,
|
||||
allowNull: true,
|
||||
references: {
|
||||
model: 'districts',
|
||||
key: 'id'
|
||||
}
|
||||
});
|
||||
console.log('✓ newDistrictId column added');
|
||||
} else {
|
||||
console.log('- newDistrictId column already exists');
|
||||
}
|
||||
|
||||
// Add newStateId column if not exists
|
||||
if (!tableInfo.newStateId) {
|
||||
console.log('Adding newStateId column...');
|
||||
await queryInterface.addColumn('relocation_requests', 'newStateId', {
|
||||
type: db.Sequelize.DataTypes.UUID,
|
||||
allowNull: true,
|
||||
references: {
|
||||
model: 'states',
|
||||
key: 'id'
|
||||
}
|
||||
});
|
||||
console.log('✓ newStateId column added');
|
||||
} else {
|
||||
console.log('- newStateId column already exists');
|
||||
}
|
||||
|
||||
// Update enum to include 'Intercity' if not already present
|
||||
console.log('Checking relocationType enum...');
|
||||
try {
|
||||
await db.sequelize.query(`
|
||||
ALTER TYPE "enum_relocation_requests_relocationType"
|
||||
ADD VALUE IF NOT EXISTS 'Intercity';
|
||||
`);
|
||||
console.log('✓ Intercity added to enum (if not already present)');
|
||||
} catch (enumError: any) {
|
||||
// PostgreSQL doesn't support IF NOT EXISTS for enum values in some versions
|
||||
if (enumError.code === '42710') {
|
||||
console.log('- Intercity already exists in enum');
|
||||
} else {
|
||||
console.log('Warning: Could not update enum:', enumError.message);
|
||||
}
|
||||
}
|
||||
|
||||
console.log('\n✅ Migration completed successfully!');
|
||||
} catch (error) {
|
||||
console.error('❌ Migration failed:', error);
|
||||
process.exit(1);
|
||||
} finally {
|
||||
await db.sequelize.close();
|
||||
}
|
||||
}
|
||||
|
||||
migrate();
|
||||
@ -1,49 +0,0 @@
|
||||
import 'dotenv/config';
|
||||
import db from '../src/database/models/index.js';
|
||||
|
||||
/**
|
||||
* Creates sla_notification_dispatches — idempotent audit log for SLA emails/alerts.
|
||||
* Safe to run multiple times.
|
||||
*/
|
||||
async function migrate() {
|
||||
const { sequelize } = db as { sequelize: { authenticate: () => Promise<void>; query: (sql: string) => Promise<unknown>; close: () => Promise<void> } };
|
||||
await sequelize.authenticate();
|
||||
console.log('Database connected.');
|
||||
|
||||
const statements = [
|
||||
`CREATE TABLE IF NOT EXISTS sla_notification_dispatches (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
"trackingId" UUID NOT NULL REFERENCES sla_tracking(id) ON DELETE CASCADE,
|
||||
"thresholdKey" VARCHAR(128) NOT NULL,
|
||||
"dispatchType" VARCHAR(32) NOT NULL,
|
||||
"templateCode" VARCHAR(64),
|
||||
"stageName" VARCHAR(255),
|
||||
"reminderId" UUID,
|
||||
"escalationLevel" INTEGER,
|
||||
"recipientCount" INTEGER NOT NULL DEFAULT 0,
|
||||
"sentAt" TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
status VARCHAR(24) NOT NULL DEFAULT 'sent',
|
||||
"createdAt" TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
"updatedAt" TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
)`,
|
||||
`CREATE UNIQUE INDEX IF NOT EXISTS sla_notification_dispatches_tracking_threshold_uq
|
||||
ON sla_notification_dispatches ("trackingId", "thresholdKey")`,
|
||||
`CREATE INDEX IF NOT EXISTS sla_notification_dispatches_tracking_sent_idx
|
||||
ON sla_notification_dispatches ("trackingId", "sentAt")`,
|
||||
`CREATE INDEX IF NOT EXISTS sla_notification_dispatches_type_idx
|
||||
ON sla_notification_dispatches ("dispatchType")`
|
||||
];
|
||||
|
||||
for (const sql of statements) {
|
||||
console.log('Running:', sql.split('\n')[0].slice(0, 72) + '...');
|
||||
await sequelize.query(sql);
|
||||
}
|
||||
|
||||
console.log('sla_notification_dispatches migration complete.');
|
||||
await sequelize.close();
|
||||
}
|
||||
|
||||
migrate().catch((err) => {
|
||||
console.error(err);
|
||||
process.exit(1);
|
||||
});
|
||||
@ -1,34 +0,0 @@
|
||||
import 'dotenv/config';
|
||||
import db from '../src/database/models/index.js';
|
||||
|
||||
/**
|
||||
* Aligns sla_tracking with SLATracking model (entity columns + metadata for reminder state).
|
||||
* Safe to run multiple times (IF NOT EXISTS).
|
||||
*/
|
||||
async function migrate() {
|
||||
const { sequelize } = db as any;
|
||||
await sequelize.authenticate();
|
||||
console.log('Database connected.');
|
||||
|
||||
const statements = [
|
||||
`ALTER TABLE sla_tracking ADD COLUMN IF NOT EXISTS "entityType" VARCHAR(255)`,
|
||||
`ALTER TABLE sla_tracking ADD COLUMN IF NOT EXISTS "entityId" UUID`,
|
||||
`ALTER TABLE sla_tracking ADD COLUMN IF NOT EXISTS metadata JSONB DEFAULT '{}'::jsonb`,
|
||||
// Backfill entity columns for legacy rows that only had applicationId
|
||||
`UPDATE sla_tracking SET "entityType" = 'application' WHERE "entityType" IS NULL AND "applicationId" IS NOT NULL`,
|
||||
`UPDATE sla_tracking SET "entityId" = "applicationId" WHERE "entityId" IS NULL AND "applicationId" IS NOT NULL`
|
||||
];
|
||||
|
||||
for (const sql of statements) {
|
||||
console.log('Running:', sql.slice(0, 80) + '...');
|
||||
await sequelize.query(sql);
|
||||
}
|
||||
|
||||
console.log('sla_tracking schema migration complete.');
|
||||
await sequelize.close();
|
||||
}
|
||||
|
||||
migrate().catch((err) => {
|
||||
console.error(err);
|
||||
process.exit(1);
|
||||
});
|
||||
@ -1,36 +1,89 @@
|
||||
/**
|
||||
* Database Migration Script
|
||||
* Synchronizes all Sequelize models with the database (PostgreSQL).
|
||||
* This script will DROP all existing tables and recreate them.
|
||||
* Database Migration Script — destructive fresh sync.
|
||||
*
|
||||
* Schema for modules such as constitutional change (ENUM values, partial unique indexes,
|
||||
* columns) is defined only on Sequelize models — no separate "table alteration" scripts are
|
||||
* required after a fresh `migrate` + `seed:all` (see package.json `setup:fresh`).
|
||||
* Drops every table and recreates the schema from Sequelize model definitions
|
||||
* in `src/database/models/`. After the fresh schema is in place, every
|
||||
* versioned migration file under `scripts/migrations/` is automatically
|
||||
* stamped into the `migrations` table as "already applied" so subsequent
|
||||
* `npm run migrate:up` runs on this DB will be no-ops until a newer
|
||||
* migration is added.
|
||||
*
|
||||
* For incremental schema changes on environments that already hold data,
|
||||
* use `npm run migrate:up` instead.
|
||||
*
|
||||
* Run: npx tsx scripts/migrate.ts
|
||||
* Flags:
|
||||
* --no-baseline Skip stamping migration files as applied (advanced).
|
||||
*/
|
||||
|
||||
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';
|
||||
|
||||
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() {
|
||||
console.log('🔄 Starting database synchronization (Fresh Startup)...\n');
|
||||
console.log('⚠️ WARNING: This will drop all existing tables in the database.\n');
|
||||
|
||||
const skipBaseline = process.argv.includes('--no-baseline');
|
||||
|
||||
try {
|
||||
// Authenticate with the database
|
||||
await db.sequelize.authenticate();
|
||||
console.log('📡 Connected to the database successfully.');
|
||||
|
||||
// Synchronize models (force: true drops existing tables)
|
||||
// This ensures that the schema exactly matches the Sequelize models
|
||||
// force: true drops existing tables — schema is rebuilt exactly from
|
||||
// Sequelize models, so every enum / column / index matches code.
|
||||
await db.sequelize.sync({ force: true });
|
||||
|
||||
console.log('\n✅ All tables created and synchronized successfully!');
|
||||
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('----------------------------------------------------');
|
||||
console.log('----------------------------------------------------\n');
|
||||
|
||||
if (!skipBaseline) {
|
||||
await baselineMigrationsTable();
|
||||
} else {
|
||||
console.log('Skipping migration baseline (--no-baseline).');
|
||||
}
|
||||
|
||||
process.exit(0);
|
||||
} catch (error: any) {
|
||||
|
||||
@ -1,63 +0,0 @@
|
||||
import { Sequelize } from 'sequelize';
|
||||
import config from '../src/common/config/database.js';
|
||||
|
||||
const env = process.env.NODE_ENV || 'development';
|
||||
const dbConfig = config[env];
|
||||
|
||||
const sequelize = new Sequelize(
|
||||
dbConfig.database,
|
||||
dbConfig.username,
|
||||
dbConfig.password,
|
||||
{
|
||||
host: dbConfig.host,
|
||||
port: dbConfig.port,
|
||||
dialect: dbConfig.dialect,
|
||||
logging: console.log
|
||||
}
|
||||
);
|
||||
|
||||
async function migrate() {
|
||||
try {
|
||||
await sequelize.authenticate();
|
||||
console.log('Connected to database.');
|
||||
|
||||
const queryInterface = sequelize.getQueryInterface();
|
||||
|
||||
// Check if users table exists
|
||||
const tables = await queryInterface.showAllTables();
|
||||
if (!tables.includes('users')) {
|
||||
console.log('Users table does not exist. Skipping rename.');
|
||||
return;
|
||||
}
|
||||
|
||||
// Rename fullName to name
|
||||
const columns = await queryInterface.describeTable('users');
|
||||
|
||||
if (columns.fullName && !columns.name) {
|
||||
console.log('Renaming fullName to name...');
|
||||
await queryInterface.renameColumn('users', 'fullName', 'name');
|
||||
} else if (columns.fullName && columns.name) {
|
||||
console.log('Both fullName and name exist. Manual intervention needed.');
|
||||
} else {
|
||||
console.log('fullName not found or name already exists.');
|
||||
}
|
||||
|
||||
// Rename mobileNumber to phone
|
||||
if (columns.mobileNumber && !columns.phone) {
|
||||
console.log('Renaming mobileNumber to phone...');
|
||||
await queryInterface.renameColumn('users', 'mobileNumber', 'phone');
|
||||
} else if (columns.mobileNumber && columns.phone) {
|
||||
console.log('Both mobileNumber and phone exist. Manual intervention needed.');
|
||||
} else {
|
||||
console.log('mobileNumber not found or phone already exists.');
|
||||
}
|
||||
|
||||
console.log('Migration successful.');
|
||||
} catch (error) {
|
||||
console.error('Migration failed:', error);
|
||||
} finally {
|
||||
await sequelize.close();
|
||||
}
|
||||
}
|
||||
|
||||
migrate();
|
||||
@ -0,0 +1,52 @@
|
||||
/**
|
||||
* Migration: create sla_notification_dispatches
|
||||
*
|
||||
* Folds the legacy `scripts/migrate-sla-notification-dispatches.ts` script
|
||||
* into the new versioned migrations system. Creates the idempotent dispatch
|
||||
* audit log used to dedupe SLA emails/alerts per tracking entry + threshold.
|
||||
*
|
||||
* Fresh `npm run migrate` runs already build this table from
|
||||
* `SLANotificationDispatch` model — this migration exists so environments
|
||||
* that predate the model can catch up via `npm run migrate:up`.
|
||||
*/
|
||||
|
||||
import type { Sequelize, Transaction } from 'sequelize';
|
||||
|
||||
export interface MigrationContext {
|
||||
sequelize: Sequelize;
|
||||
transaction: Transaction;
|
||||
}
|
||||
|
||||
const migration = {
|
||||
async up({ sequelize, transaction }: MigrationContext): Promise<void> {
|
||||
const statements = [
|
||||
`CREATE TABLE IF NOT EXISTS sla_notification_dispatches (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
"trackingId" UUID NOT NULL REFERENCES sla_tracking(id) ON DELETE CASCADE,
|
||||
"thresholdKey" VARCHAR(128) NOT NULL,
|
||||
"dispatchType" VARCHAR(32) NOT NULL,
|
||||
"templateCode" VARCHAR(64),
|
||||
"stageName" VARCHAR(255),
|
||||
"reminderId" UUID,
|
||||
"escalationLevel" INTEGER,
|
||||
"recipientCount" INTEGER NOT NULL DEFAULT 0,
|
||||
"sentAt" TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
status VARCHAR(24) NOT NULL DEFAULT 'sent',
|
||||
"createdAt" TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
"updatedAt" TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
)`,
|
||||
`CREATE UNIQUE INDEX IF NOT EXISTS sla_notification_dispatches_tracking_threshold_uq
|
||||
ON sla_notification_dispatches ("trackingId", "thresholdKey")`,
|
||||
`CREATE INDEX IF NOT EXISTS sla_notification_dispatches_tracking_sent_idx
|
||||
ON sla_notification_dispatches ("trackingId", "sentAt")`,
|
||||
`CREATE INDEX IF NOT EXISTS sla_notification_dispatches_type_idx
|
||||
ON sla_notification_dispatches ("dispatchType")`
|
||||
];
|
||||
|
||||
for (const sql of statements) {
|
||||
await sequelize.query(sql, { transaction });
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export default migration;
|
||||
58
scripts/migrations/README.md
Normal file
58
scripts/migrations/README.md
Normal file
@ -0,0 +1,58 @@
|
||||
# Database Migrations
|
||||
|
||||
This folder holds versioned, incremental database migrations for environments
|
||||
that already have a populated schema (UAT / production). On a fresh dev box,
|
||||
`npm run migrate` (destructive `sync({ force: true })`) is still the fastest
|
||||
route — the model definitions in `src/database/models/` remain the source of
|
||||
truth for the desired schema.
|
||||
|
||||
## Workflow
|
||||
|
||||
| Goal | Command |
|
||||
|-----------------------------------------------------|---------|
|
||||
| Fresh / dev: drop everything, recreate from models | `npm run migrate` |
|
||||
| Mark every migration file here as already-applied | `npm run migrate:baseline` |
|
||||
| Apply only the migrations not yet recorded in DB | `npm run migrate:up` |
|
||||
| List applied vs pending | `npm run migrate:status` |
|
||||
| Scaffold a new migration file | `npm run migrate:create -- <snake_case_name>` |
|
||||
|
||||
A typical post-fresh-setup sequence is therefore:
|
||||
|
||||
```bash
|
||||
npm run migrate # drop + recreate
|
||||
npm run migrate:baseline # stamp this folder's files as already applied
|
||||
npm run seed:all # seed master data
|
||||
```
|
||||
|
||||
After every subsequent deploy on the same DB:
|
||||
|
||||
```bash
|
||||
npm run migrate:up # apply only the new migrations
|
||||
```
|
||||
|
||||
## File naming convention
|
||||
|
||||
```
|
||||
<YYYYMMDDHHMMSS>_<snake_case_description>.ts
|
||||
```
|
||||
|
||||
Examples:
|
||||
- `20260526143000_add_finance_kyc_column.ts`
|
||||
- `20260601090000_drop_legacy_questionnaire_column.ts`
|
||||
|
||||
The runner sorts files lexicographically, so the timestamp prefix dictates
|
||||
execution order. Always use UTC when manually creating timestamps; the
|
||||
scaffolder (`npm run migrate:create`) emits a current-time UTC stamp for you.
|
||||
|
||||
## Authoring a migration
|
||||
|
||||
1. Run `npm run migrate:create -- add_finance_kyc_column`.
|
||||
2. Edit the generated file — implement `up({ sequelize })`.
|
||||
3. Update the corresponding Sequelize model so fresh `migrate` runs produce
|
||||
the same end state.
|
||||
4. Commit both the migration and the model changes in the same PR.
|
||||
5. On every environment that holds real data, run `npm run migrate:up`.
|
||||
|
||||
The runner records each successful migration in the `migrations` table
|
||||
(`{ name, appliedAt, checksum }`) so re-runs are safe and idempotent at the
|
||||
runner level — independently of the migration's own SQL.
|
||||
42
scripts/migrations/_template.ts
Normal file
42
scripts/migrations/_template.ts
Normal file
@ -0,0 +1,42 @@
|
||||
/**
|
||||
* Migration template — copy into a new file via `npm run migrate:create -- <name>`.
|
||||
*
|
||||
* File naming convention: `<YYYYMMDDHHMMSS>_<snake_case_description>.ts`
|
||||
* e.g. `20260526143000_add_finance_kyc_column.ts`
|
||||
*
|
||||
* The runner (`scripts/run-migrations.ts`) imports the default export and
|
||||
* invokes `up()`. After `up()` resolves it records the filename (without
|
||||
* extension) in the `migrations` table so the migration is never re-run on
|
||||
* this environment.
|
||||
*
|
||||
* Guidelines:
|
||||
* - Always wrap multi-statement changes in a single Sequelize transaction.
|
||||
* - Prefer idempotent DDL (`IF NOT EXISTS`, `IF EXISTS`) so accidental
|
||||
* re-runs are safe.
|
||||
* - Never destructively drop columns/tables that hold real production data
|
||||
* unless you have a separate, explicit data-migration step.
|
||||
* - Update the corresponding Sequelize model in `src/database/models/`
|
||||
* in the same PR — migrations are a delta for environments that already
|
||||
* have a populated schema; the model definitions remain the source of
|
||||
* truth for fresh `npm run migrate` builds.
|
||||
*/
|
||||
|
||||
import type { QueryInterface, Sequelize } from 'sequelize';
|
||||
|
||||
export interface MigrationContext {
|
||||
queryInterface: QueryInterface;
|
||||
sequelize: Sequelize;
|
||||
}
|
||||
|
||||
const migration = {
|
||||
async up({ sequelize }: MigrationContext): Promise<void> {
|
||||
// Example:
|
||||
// await sequelize.query(`
|
||||
// ALTER TABLE "applications"
|
||||
// ADD COLUMN IF NOT EXISTS "kycReviewedAt" TIMESTAMPTZ NULL;
|
||||
// `);
|
||||
throw new Error('Migration template — implement up() before running.');
|
||||
}
|
||||
};
|
||||
|
||||
export default migration;
|
||||
@ -1,36 +0,0 @@
|
||||
|
||||
import dotenv from 'dotenv';
|
||||
import path from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
dotenv.config({ path: path.join(__dirname, '../.env') });
|
||||
|
||||
import db from '../src/database/models/index.js';
|
||||
|
||||
async function run() {
|
||||
try {
|
||||
const appId = '1f1fec7d-7034-4588-a4b2-0e1d4cc3f149';
|
||||
const abhishekId = '9284a190-f4d2-49f3-9186-bb7c93dc9b6d';
|
||||
|
||||
const deleted = await db.RequestParticipant.destroy({
|
||||
where: {
|
||||
requestId: appId,
|
||||
userId: abhishekId
|
||||
}
|
||||
});
|
||||
|
||||
if (deleted) {
|
||||
console.log('Successfully removed Abhishek from application participants.');
|
||||
} else {
|
||||
console.log('Abhishek was not found in participants for this application.');
|
||||
}
|
||||
|
||||
process.exit(0);
|
||||
} catch (error) {
|
||||
console.error('Error:', error);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
run();
|
||||
181
scripts/run-migrations.ts
Normal file
181
scripts/run-migrations.ts
Normal file
@ -0,0 +1,181 @@
|
||||
/**
|
||||
* Migration runner.
|
||||
*
|
||||
* Usage:
|
||||
* npm run migrate:up -> apply pending migrations
|
||||
* npm run migrate:status -> list applied vs pending
|
||||
* npm run migrate:baseline -> mark every existing migration as already applied
|
||||
*
|
||||
* The runner is intentionally minimal: it discovers `.ts` files inside
|
||||
* `scripts/migrations/`, ignores anything starting with `_` (template files,
|
||||
* shared helpers), sorts them lexicographically (filename-based ordering ==
|
||||
* timestamp ordering when authors follow the convention), and applies each
|
||||
* file whose `name` is not yet recorded in the `migrations` table.
|
||||
*
|
||||
* Each migration file must `export default` an object exposing
|
||||
* async up({ sequelize, queryInterface }): Promise<void>
|
||||
* See `scripts/migrations/_template.ts` for the contract.
|
||||
*/
|
||||
|
||||
import 'dotenv/config';
|
||||
import { fileURLToPath, pathToFileURL } from 'url';
|
||||
import { createHash } from 'crypto';
|
||||
import { promises as fs } from 'fs';
|
||||
import path from 'path';
|
||||
import db from '../src/database/models/index.js';
|
||||
|
||||
const MIGRATIONS_DIR = path.resolve(
|
||||
path.dirname(fileURLToPath(import.meta.url)),
|
||||
'migrations'
|
||||
);
|
||||
|
||||
type Mode = 'up' | 'status' | 'baseline';
|
||||
|
||||
function parseMode(argv: string[]): Mode {
|
||||
if (argv.includes('--status')) return 'status';
|
||||
if (argv.includes('--baseline')) return 'baseline';
|
||||
return 'up';
|
||||
}
|
||||
|
||||
async function discoverMigrationFiles(): Promise<string[]> {
|
||||
const entries = await fs.readdir(MIGRATIONS_DIR);
|
||||
return entries
|
||||
.filter((name) => name.endsWith('.ts') && !name.startsWith('_'))
|
||||
.sort();
|
||||
}
|
||||
|
||||
async function ensureMigrationsTable(): Promise<void> {
|
||||
// The Migration model has the canonical schema. We create the table only
|
||||
// if it does not yet exist so this script remains safe to run on any
|
||||
// environment (fresh sync OR already-migrated DB).
|
||||
await db.Migration.sync();
|
||||
}
|
||||
|
||||
async function loadMigration(file: string): Promise<{ up: (ctx: any) => Promise<void> }> {
|
||||
const fullPath = path.join(MIGRATIONS_DIR, file);
|
||||
const mod: any = await import(pathToFileURL(fullPath).href);
|
||||
const migration = mod.default ?? mod;
|
||||
if (!migration || typeof migration.up !== 'function') {
|
||||
throw new Error(`Migration ${file} does not export a default { up } function.`);
|
||||
}
|
||||
return migration;
|
||||
}
|
||||
|
||||
async function fileChecksum(file: string): Promise<string> {
|
||||
const buf = await fs.readFile(path.join(MIGRATIONS_DIR, file));
|
||||
return createHash('sha256').update(buf).digest('hex');
|
||||
}
|
||||
|
||||
async function listApplied(): Promise<Set<string>> {
|
||||
const rows = await db.Migration.findAll({ attributes: ['name'] });
|
||||
return new Set(rows.map((r: any) => r.name as string));
|
||||
}
|
||||
|
||||
function stripExt(file: string): string {
|
||||
return file.replace(/\.ts$/, '');
|
||||
}
|
||||
|
||||
async function runUp(): Promise<void> {
|
||||
const files = await discoverMigrationFiles();
|
||||
const applied = await listApplied();
|
||||
|
||||
const pending = files.filter((f) => !applied.has(stripExt(f)));
|
||||
if (pending.length === 0) {
|
||||
console.log('All migrations are already applied. Nothing to do.');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`Applying ${pending.length} migration(s)...\n`);
|
||||
for (const file of pending) {
|
||||
const name = stripExt(file);
|
||||
process.stdout.write(` → ${name} ... `);
|
||||
try {
|
||||
const migration = await loadMigration(file);
|
||||
await db.sequelize.transaction(async (transaction: any) => {
|
||||
await migration.up({
|
||||
sequelize: db.sequelize,
|
||||
queryInterface: db.sequelize.getQueryInterface(),
|
||||
transaction
|
||||
});
|
||||
});
|
||||
const checksum = await fileChecksum(file);
|
||||
await db.Migration.create({ name, checksum });
|
||||
console.log('OK');
|
||||
} catch (err: any) {
|
||||
console.log('FAILED');
|
||||
console.error(`\n${file} failed:`, err.message || err);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
console.log('\nMigrations complete.');
|
||||
}
|
||||
|
||||
async function runStatus(): Promise<void> {
|
||||
const files = await discoverMigrationFiles();
|
||||
const applied = await listApplied();
|
||||
|
||||
if (files.length === 0) {
|
||||
console.log('No migration files in scripts/migrations/.');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('Migration status:');
|
||||
for (const file of files) {
|
||||
const name = stripExt(file);
|
||||
const mark = applied.has(name) ? '✓' : ' ';
|
||||
console.log(` [${mark}] ${name}`);
|
||||
}
|
||||
|
||||
const pendingCount = files.filter((f) => !applied.has(stripExt(f))).length;
|
||||
console.log(`\nApplied: ${files.length - pendingCount} Pending: ${pendingCount}`);
|
||||
}
|
||||
|
||||
async function runBaseline(): Promise<void> {
|
||||
const files = await discoverMigrationFiles();
|
||||
const applied = await listApplied();
|
||||
|
||||
const toStamp = files.filter((f) => !applied.has(stripExt(f)));
|
||||
if (toStamp.length === 0) {
|
||||
console.log('Baseline: every migration is already recorded. Nothing to do.');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`Stamping ${toStamp.length} migration(s) as already-applied (no SQL executed):\n`);
|
||||
for (const file of toStamp) {
|
||||
const name = stripExt(file);
|
||||
const checksum = await fileChecksum(file);
|
||||
await db.Migration.create({ name, checksum });
|
||||
console.log(` + ${name}`);
|
||||
}
|
||||
console.log('\nBaseline complete.');
|
||||
}
|
||||
|
||||
async function main(): Promise<void> {
|
||||
const mode = parseMode(process.argv.slice(2));
|
||||
|
||||
try {
|
||||
await db.sequelize.authenticate();
|
||||
await ensureMigrationsTable();
|
||||
|
||||
if (mode === 'status') {
|
||||
await runStatus();
|
||||
} else if (mode === 'baseline') {
|
||||
await runBaseline();
|
||||
} else {
|
||||
await runUp();
|
||||
}
|
||||
|
||||
await db.sequelize.close();
|
||||
process.exit(0);
|
||||
} catch (err: any) {
|
||||
console.error('\nMigration runner aborted:', err?.message || err);
|
||||
try {
|
||||
await db.sequelize.close();
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
main();
|
||||
@ -1,21 +0,0 @@
|
||||
|
||||
import db from '../src/database/models/index.js';
|
||||
const { Area, District, User } = db;
|
||||
|
||||
async function testAreas() {
|
||||
try {
|
||||
console.log('Testing Area.findAll...');
|
||||
const areas = await Area.findAll({
|
||||
include: [
|
||||
{ model: District, as: 'district', attributes: ['districtName'] },
|
||||
{ model: User, as: 'manager', attributes: ['id', 'fullName', 'email', 'mobileNumber'] }
|
||||
],
|
||||
order: [['areaName', 'ASC']]
|
||||
});
|
||||
console.log('Successfully fetched areas:', JSON.stringify(areas, null, 2));
|
||||
} catch (error) {
|
||||
console.error('Error fetching areas:', error);
|
||||
}
|
||||
}
|
||||
|
||||
testAreas();
|
||||
@ -1,34 +0,0 @@
|
||||
|
||||
import db from '../src/database/models/index.js';
|
||||
const { Region, Zone, State, User } = db;
|
||||
|
||||
async function testRegions() {
|
||||
try {
|
||||
console.log('Testing Region.findAll...');
|
||||
const regions = await Region.findAll({
|
||||
include: [
|
||||
{
|
||||
model: State,
|
||||
as: 'states',
|
||||
attributes: ['id', 'stateName']
|
||||
},
|
||||
{
|
||||
model: Zone,
|
||||
as: 'zone',
|
||||
attributes: ['id', 'zoneName']
|
||||
},
|
||||
{
|
||||
model: User,
|
||||
as: 'regionalManager',
|
||||
attributes: ['id', 'fullName', 'email', 'mobileNumber']
|
||||
}
|
||||
],
|
||||
order: [['regionName', 'ASC']]
|
||||
});
|
||||
console.log('Successfully fetched regions:', JSON.stringify(regions, null, 2));
|
||||
} catch (error) {
|
||||
console.error('Error fetching regions:', error);
|
||||
}
|
||||
}
|
||||
|
||||
testRegions();
|
||||
@ -1,26 +0,0 @@
|
||||
import db from '../src/database/models/index.js';
|
||||
|
||||
async function testInsert() {
|
||||
try {
|
||||
// Attempt insert without checking existing records
|
||||
// If it fails with "invalid input value", the enum is truly not updated.
|
||||
// If it fails with "foreign key", the enum was VALID but the data was wrong.
|
||||
await db.sequelize.query(`
|
||||
DO $$
|
||||
BEGIN
|
||||
-- This will fail if 'architecture' is invalid for the enum
|
||||
PERFORM 'architecture'::"enum_request_participants_participantType";
|
||||
RAISE NOTICE '✅ Enum check passed!';
|
||||
EXCEPTION WHEN OTHERS THEN
|
||||
RAISE EXCEPTION '❌ Enum check failed: %', SQLERRM;
|
||||
END $$;
|
||||
`);
|
||||
console.log('✅ PL/pgSQL Enum check passed!');
|
||||
} catch (error: any) {
|
||||
console.error(error.message);
|
||||
} finally {
|
||||
process.exit(0);
|
||||
}
|
||||
}
|
||||
|
||||
testInsert();
|
||||
@ -1,22 +0,0 @@
|
||||
import db from '../src/database/models/index.js';
|
||||
|
||||
async function testInsert() {
|
||||
try {
|
||||
const testId = '00000000-0000-0000-0000-000000000000';
|
||||
await db.sequelize.query(`
|
||||
INSERT INTO request_participants ("id", "requestId", "requestType", "userId", "participantType", "joinedMethod", "createdAt", "updatedAt")
|
||||
VALUES (gen_random_uuid(), '${testId}', 'test', '9950ee60-ddf6-4091-a1e6-e7161e6d8bb6', 'architecture', 'manual', now(), now())
|
||||
`);
|
||||
console.log('✅ Manual insert successful!');
|
||||
|
||||
// Clean up
|
||||
await db.sequelize.query(`DELETE FROM request_participants WHERE "requestId" = '${testId}'`);
|
||||
console.log('✅ Clean up successful!');
|
||||
} catch (error: any) {
|
||||
console.error('❌ Manual insert failed:', error.message);
|
||||
} finally {
|
||||
process.exit(0);
|
||||
}
|
||||
}
|
||||
|
||||
testInsert();
|
||||
@ -1,42 +0,0 @@
|
||||
|
||||
import 'dotenv/config';
|
||||
import db from '../src/database/models/index.js';
|
||||
|
||||
const updateEnum = async () => {
|
||||
try {
|
||||
console.log('>>> STARTING ENUM MIGRATION <<<');
|
||||
await db.sequelize.authenticate();
|
||||
console.log('Database connection established.');
|
||||
|
||||
// Raw query to add values to enum
|
||||
// Note: PostgreSQL cannot remove enum values, only add.
|
||||
// We will add the "Interview Pending" variations.
|
||||
|
||||
const queryInterface = db.sequelize.getQueryInterface();
|
||||
|
||||
try {
|
||||
await db.sequelize.query(`ALTER TYPE "enum_applications_overallStatus" ADD VALUE IF NOT EXISTS 'Level 1 Interview Pending';`);
|
||||
console.log('Added Level 1 Interview Pending');
|
||||
} catch (e) { console.log('Level 1 Interview Pending likely exists or error', e.message); }
|
||||
|
||||
try {
|
||||
await db.sequelize.query(`ALTER TYPE "enum_applications_overallStatus" ADD VALUE IF NOT EXISTS 'Level 2 Interview Pending';`);
|
||||
console.log('Added Level 2 Interview Pending');
|
||||
} catch (e) { console.log('Level 2 Interview Pending likely exists or error', e.message); }
|
||||
|
||||
try {
|
||||
await db.sequelize.query(`ALTER TYPE "enum_applications_overallStatus" ADD VALUE IF NOT EXISTS 'Level 3 Interview Pending';`);
|
||||
console.log('Added Level 3 Interview Pending');
|
||||
} catch (e) { console.log('Level 3 Interview Pending likely exists or error', e.message); }
|
||||
|
||||
console.log('>>> SUCCESS: Enum values updated <<<');
|
||||
|
||||
await db.sequelize.close();
|
||||
process.exit(0);
|
||||
} catch (error) {
|
||||
console.error('>>> ERROR: Failed to update Enum', error);
|
||||
process.exit(1);
|
||||
}
|
||||
};
|
||||
|
||||
updateEnum();
|
||||
@ -1,46 +0,0 @@
|
||||
import db from '../src/database/models/index.js';
|
||||
const { sequelize } = db;
|
||||
|
||||
async function updateDealerCodesTable() {
|
||||
console.log('🔄 Checking and updating dealer_codes table schema...');
|
||||
|
||||
try {
|
||||
// Add applicationId
|
||||
await sequelize.query(`
|
||||
DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name='dealer_codes' AND column_name='applicationId') THEN
|
||||
ALTER TABLE dealer_codes ADD COLUMN "applicationId" UUID REFERENCES applications(id);
|
||||
END IF;
|
||||
|
||||
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name='dealer_codes' AND column_name='salesCode') THEN
|
||||
ALTER TABLE dealer_codes ADD COLUMN "salesCode" VARCHAR(255);
|
||||
END IF;
|
||||
|
||||
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name='dealer_codes' AND column_name='serviceCode') THEN
|
||||
ALTER TABLE dealer_codes ADD COLUMN "serviceCode" VARCHAR(255);
|
||||
END IF;
|
||||
|
||||
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name='dealer_codes' AND column_name='gmaCode') THEN
|
||||
ALTER TABLE dealer_codes ADD COLUMN "gmaCode" VARCHAR(255);
|
||||
END IF;
|
||||
|
||||
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name='dealer_codes' AND column_name='gearCode') THEN
|
||||
ALTER TABLE dealer_codes ADD COLUMN "gearCode" VARCHAR(255);
|
||||
END IF;
|
||||
|
||||
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name='dealer_codes' AND column_name='sapMasterId') THEN
|
||||
ALTER TABLE dealer_codes ADD COLUMN "sapMasterId" VARCHAR(255);
|
||||
END IF;
|
||||
END $$;
|
||||
`);
|
||||
|
||||
console.log('✅ dealer_codes table schema updated successfully.');
|
||||
process.exit(0);
|
||||
} catch (error) {
|
||||
console.error('❌ Error updating dealer_codes table:', error);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
updateDealerCodesTable();
|
||||
@ -1,33 +0,0 @@
|
||||
import { APPLICATION_STATUS } from '../src/common/config/constants.js';
|
||||
import db from '../src/database/models/index.js';
|
||||
|
||||
async function updateEnum() {
|
||||
try {
|
||||
console.log('🔄 Syncing all APPLICATION_STATUS values with DB Enum...');
|
||||
|
||||
const statuses = Object.values(APPLICATION_STATUS);
|
||||
|
||||
for (const status of statuses) {
|
||||
try {
|
||||
// Posgres doesn't support IF NOT EXISTS for ADD VALUE in 9.5 and below
|
||||
// so we do it one by one and ignore "already exists" errors.
|
||||
await db.sequelize.query(`ALTER TYPE "enum_applications_overallStatus" ADD VALUE '${status}'`);
|
||||
console.log(`✅ Added: ${status}`);
|
||||
} catch (e: any) {
|
||||
if (e.message.includes('already exists')) {
|
||||
// console.log(`ℹ️ Already exists: ${status}`);
|
||||
} else {
|
||||
console.error(`❌ Error adding ${status}:`, e.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log('\n✅ Database Enum successfully synchronized with constants.');
|
||||
} catch (error: any) {
|
||||
console.error('❌ Critical failure during sync:', error.message);
|
||||
} finally {
|
||||
process.exit(0);
|
||||
}
|
||||
}
|
||||
|
||||
updateEnum();
|
||||
@ -1,26 +0,0 @@
|
||||
import db from '../src/database/models/index.js';
|
||||
|
||||
async function updateParticipantEnum() {
|
||||
try {
|
||||
console.log('🔄 Adding "architecture" to participantType enum...');
|
||||
|
||||
try {
|
||||
await db.sequelize.query(`ALTER TYPE "enum_request_participants_participantType" ADD VALUE 'architecture'`);
|
||||
console.log(`✅ Added: architecture`);
|
||||
} catch (e: any) {
|
||||
if (e.message.includes('already exists')) {
|
||||
console.log(`ℹ️ Already exists: architecture`);
|
||||
} else {
|
||||
console.error(`❌ Error adding architecture:`, e.message);
|
||||
}
|
||||
}
|
||||
|
||||
console.log('\n✅ Database Enum successfully updated.');
|
||||
} catch (error: any) {
|
||||
console.error('❌ Critical failure:', error.message);
|
||||
} finally {
|
||||
process.exit(0);
|
||||
}
|
||||
}
|
||||
|
||||
updateParticipantEnum();
|
||||
@ -1,47 +0,0 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import {
|
||||
getResignationStatusForStage,
|
||||
getTerminationStatusForStage,
|
||||
normalizeClearanceStatus,
|
||||
normalizeFnFStatus,
|
||||
normalizeTerminationCurrentStage,
|
||||
getLegacyTerminationRowFixes
|
||||
} from '../src/common/utils/offboardingStatus.js';
|
||||
import { getJointRoundCutoffMsFromTimeline } from '../src/common/utils/terminationJointReviewRound.util.js';
|
||||
|
||||
assert.equal(normalizeFnFStatus('settled'), 'Completed');
|
||||
assert.equal(normalizeFnFStatus('finance approval'), 'Finance Approval');
|
||||
|
||||
assert.equal(getResignationStatusForStage('ASM'), 'ASM Review');
|
||||
assert.equal(getResignationStatusForStage('F&F Initiated'), 'F&F Initiated');
|
||||
|
||||
assert.equal(getTerminationStatusForStage('Submitted'), 'Submitted');
|
||||
assert.equal(getTerminationStatusForStage('Terminated'), 'Terminated');
|
||||
|
||||
assert.equal(
|
||||
normalizeTerminationCurrentStage('Personal Hearing'),
|
||||
'Evaluation of Dealer SCN Response'
|
||||
);
|
||||
assert.deepEqual(getLegacyTerminationRowFixes({ currentStage: 'Personal Hearing', status: 'Personal Hearing Pending' }), {
|
||||
currentStage: 'Evaluation of Dealer SCN Response',
|
||||
status: 'SCN Response Evaluation Pending'
|
||||
});
|
||||
|
||||
const reconsiderTimeline = [
|
||||
{ action: 'Approved', targetStage: 'NBH Final Approval', timestamp: new Date('2024-01-01').toISOString() },
|
||||
{
|
||||
action: 'Sent for Reconsideration',
|
||||
targetStage: 'Evaluation of Dealer SCN Response',
|
||||
timestamp: new Date('2025-06-15T12:00:00.000Z').toISOString()
|
||||
}
|
||||
];
|
||||
assert.equal(
|
||||
getJointRoundCutoffMsFromTimeline(reconsiderTimeline, 'scn_response_eval'),
|
||||
new Date('2025-06-15T12:00:00.000Z').getTime()
|
||||
);
|
||||
|
||||
assert.equal(normalizeClearanceStatus('Cleared', 0), 'NOC Submitted');
|
||||
assert.equal(normalizeClearanceStatus('Cleared', 100), 'Dues Pending');
|
||||
assert.equal(normalizeClearanceStatus('Pending', 0), 'Pending');
|
||||
|
||||
console.log('Offboarding status normalization checks passed.');
|
||||
@ -1,58 +0,0 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import {
|
||||
validateOffboardingAction,
|
||||
getPreviousStage,
|
||||
getOffboardingAuditAction
|
||||
} from '../src/common/utils/offboardingWorkflow.utils.js';
|
||||
import {
|
||||
OFFBOARDING_ACTIONS,
|
||||
REQUEST_TYPES,
|
||||
TERMINATION_STAGES,
|
||||
RESIGNATION_STAGES,
|
||||
CONSTITUTIONAL_STAGES,
|
||||
AUDIT_ACTIONS
|
||||
} from '../src/common/config/constants.js';
|
||||
|
||||
console.log('--- Testing Standardized Offboarding Utilities ---');
|
||||
|
||||
// 1. Test validateOffboardingAction
|
||||
console.log('Testing validateOffboardingAction...');
|
||||
assert.deepEqual(validateOffboardingAction(OFFBOARDING_ACTIONS.APPROVE, ''), { valid: true });
|
||||
assert.deepEqual(validateOffboardingAction(OFFBOARDING_ACTIONS.SEND_BACK, 'Short'), { valid: true }); // 'Short' is 5 chars
|
||||
assert.equal(validateOffboardingAction(OFFBOARDING_ACTIONS.SEND_BACK, 'No').valid, false);
|
||||
assert.equal(validateOffboardingAction(OFFBOARDING_ACTIONS.REVOKE, '').valid, false);
|
||||
assert.equal(validateOffboardingAction(OFFBOARDING_ACTIONS.REJECT, '').valid, true); // Remarks not mandatory for reject in current util choice
|
||||
console.log('✓ validateOffboardingAction passed.');
|
||||
|
||||
// 2. Test getPreviousStage - Termination
|
||||
console.log('Testing getPreviousStage (Termination)...');
|
||||
assert.equal(getPreviousStage(REQUEST_TYPES.TERMINATION, TERMINATION_STAGES.RBM_REVIEW), TERMINATION_STAGES.SUBMITTED);
|
||||
assert.equal(getPreviousStage(REQUEST_TYPES.TERMINATION, TERMINATION_STAGES.ZBH_REVIEW), TERMINATION_STAGES.RBM_REVIEW);
|
||||
assert.equal(getPreviousStage(REQUEST_TYPES.TERMINATION, TERMINATION_STAGES.TERMINATED), TERMINATION_STAGES.LEGAL_LETTER);
|
||||
console.log('✓ Termination stage resolution passed.');
|
||||
|
||||
// 3. Test getPreviousStage - Resignation
|
||||
console.log('Testing getPreviousStage (Resignation)...');
|
||||
assert.equal(getPreviousStage(REQUEST_TYPES.RESIGNATION, RESIGNATION_STAGES.RBM), RESIGNATION_STAGES.ASM);
|
||||
assert.equal(getPreviousStage(REQUEST_TYPES.RESIGNATION, RESIGNATION_STAGES.ZBH), RESIGNATION_STAGES.RBM);
|
||||
assert.equal(getPreviousStage(REQUEST_TYPES.RESIGNATION, RESIGNATION_STAGES.FNF_INITIATED), RESIGNATION_STAGES.AWAITING_FNF);
|
||||
assert.equal(getPreviousStage(REQUEST_TYPES.RESIGNATION, RESIGNATION_STAGES.DD_ADMIN), RESIGNATION_STAGES.LEGAL);
|
||||
assert.equal(getPreviousStage(REQUEST_TYPES.RESIGNATION, RESIGNATION_STAGES.COMPLETED), RESIGNATION_STAGES.FNF_INITIATED);
|
||||
console.log('✓ Resignation stage resolution passed.');
|
||||
|
||||
// 4. Test getPreviousStage - Constitutional
|
||||
console.log('Testing getPreviousStage (Constitutional)...');
|
||||
assert.equal(getPreviousStage(REQUEST_TYPES.CONSTITUTIONAL, CONSTITUTIONAL_STAGES.ASM_REVIEW), CONSTITUTIONAL_STAGES.SUBMITTED);
|
||||
assert.equal(getPreviousStage(REQUEST_TYPES.CONSTITUTIONAL, CONSTITUTIONAL_STAGES.ZM_RBM_REVIEW), CONSTITUTIONAL_STAGES.ASM_REVIEW);
|
||||
assert.equal(getPreviousStage(REQUEST_TYPES.CONSTITUTIONAL, CONSTITUTIONAL_STAGES.COMPLETED), CONSTITUTIONAL_STAGES.LEGAL_REVIEW);
|
||||
console.log('✓ Constitutional stage resolution passed.');
|
||||
|
||||
// 5. Test getOffboardingAuditAction mapping
|
||||
console.log('Testing getOffboardingAuditAction...');
|
||||
assert.equal(getOffboardingAuditAction('Sent Back', REQUEST_TYPES.TERMINATION), AUDIT_ACTIONS.UPDATED);
|
||||
assert.equal(getOffboardingAuditAction('Revoke', REQUEST_TYPES.RESIGNATION), AUDIT_ACTIONS.UPDATED);
|
||||
assert.equal(getOffboardingAuditAction('Approve', REQUEST_TYPES.CONSTITUTIONAL), AUDIT_ACTIONS.APPROVED);
|
||||
assert.equal(getOffboardingAuditAction('REJECT', REQUEST_TYPES.TERMINATION), AUDIT_ACTIONS.REJECTED);
|
||||
console.log('✓ Audit action mapping passed.');
|
||||
|
||||
console.log('\nALL STANDARDIZATION UTILITY CHECKS PASSED SUCCESSFULLY.');
|
||||
@ -108,6 +108,9 @@ import createWorknote from './activity/Worknote.js';
|
||||
import createWorkNoteAttachment from './activity/WorkNoteAttachment.js';
|
||||
import createWorkNoteTag from './activity/WorkNoteTag.js';
|
||||
|
||||
// System
|
||||
import createMigration from './system/Migration.js';
|
||||
|
||||
const env = process.env.NODE_ENV || 'development';
|
||||
const dbConfig = config[env];
|
||||
|
||||
@ -233,6 +236,9 @@ db.StageApprovalPolicy = createStageApprovalPolicy(sequelize);
|
||||
db.StageApprovalAction = createStageApprovalAction(sequelize);
|
||||
db.SystemConfiguration = createSystemConfiguration(sequelize);
|
||||
|
||||
// Batch 9: System internals
|
||||
db.Migration = createMigration(sequelize);
|
||||
|
||||
// Define associations
|
||||
Object.keys(db).forEach((modelName) => {
|
||||
if (db[modelName].associate) {
|
||||
|
||||
73
src/database/models/system/Migration.ts
Normal file
73
src/database/models/system/Migration.ts
Normal file
@ -0,0 +1,73 @@
|
||||
import { Model, DataTypes, Sequelize } from 'sequelize';
|
||||
|
||||
/**
|
||||
* Migration
|
||||
* -----------
|
||||
* Tracks which database migrations have been applied to this environment.
|
||||
*
|
||||
* Workflow:
|
||||
* - `npm run migrate` -> destructive fresh sync (drops everything and
|
||||
* recreates schema from Sequelize models).
|
||||
* - `npm run migrate:baseline` -> stamps every existing migration file under
|
||||
* `scripts/migrations/` as already-applied
|
||||
* (use this immediately after a fresh sync so
|
||||
* future incremental runs don't re-execute
|
||||
* them on top of an already-correct schema).
|
||||
* - `npm run migrate:up` -> applies any migration file under
|
||||
* `scripts/migrations/` that is not yet
|
||||
* recorded in this table.
|
||||
* - `npm run migrate:status` -> lists applied vs pending migrations.
|
||||
* - `npm run migrate:create` -> scaffolds a new `<ts>_<name>.ts` file.
|
||||
*
|
||||
* `name` is the canonical filename (without extension) so it remains stable
|
||||
* even if the file is moved between directories.
|
||||
*/
|
||||
export interface MigrationAttributes {
|
||||
id: string;
|
||||
name: string;
|
||||
appliedAt: Date;
|
||||
checksum: string | null;
|
||||
}
|
||||
|
||||
export interface MigrationInstance
|
||||
extends Model<MigrationAttributes>,
|
||||
MigrationAttributes { }
|
||||
|
||||
export default (sequelize: Sequelize) => {
|
||||
const Migration = sequelize.define<MigrationInstance>(
|
||||
'Migration',
|
||||
{
|
||||
id: {
|
||||
type: DataTypes.UUID,
|
||||
defaultValue: DataTypes.UUIDV4,
|
||||
primaryKey: true
|
||||
},
|
||||
name: {
|
||||
type: DataTypes.STRING(255),
|
||||
allowNull: false,
|
||||
unique: true
|
||||
},
|
||||
appliedAt: {
|
||||
type: DataTypes.DATE,
|
||||
allowNull: false,
|
||||
defaultValue: DataTypes.NOW
|
||||
},
|
||||
checksum: {
|
||||
// Optional SHA-256 of the migration file contents, useful to detect
|
||||
// edits to an already-applied migration during code review.
|
||||
type: DataTypes.STRING(64),
|
||||
allowNull: true
|
||||
}
|
||||
},
|
||||
{
|
||||
tableName: 'migrations',
|
||||
timestamps: false,
|
||||
indexes: [
|
||||
{ unique: true, fields: ['name'] },
|
||||
{ fields: ['appliedAt'] }
|
||||
]
|
||||
}
|
||||
);
|
||||
|
||||
return Migration;
|
||||
};
|
||||
@ -784,23 +784,12 @@ export const takeAction = async (req: AuthRequest, res: Response) => {
|
||||
newCurrentStage = RELOCATION_STAGES.REJECTED;
|
||||
}
|
||||
|
||||
// SRS §12.2.8 — enforce mandatory document submission + verification before late-stage approvals
|
||||
if (
|
||||
normalizedAction === 'APPROVE' &&
|
||||
(
|
||||
request.currentStage === RELOCATION_STAGES.NBH_APPROVAL ||
|
||||
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
|
||||
});
|
||||
}
|
||||
}
|
||||
// SRS §12.2.8 — document upload/verification is tracked for readiness display
|
||||
// (see getRelocationDocumentReadiness + progress calculation) but is NOT a hard
|
||||
// blocker on NBH Approval / Legal Clearance. Per business decision, these are
|
||||
// senior approval authorities who retain discretion to proceed even when the
|
||||
// readiness panel still shows missing uploads or pending verifications. The
|
||||
// readiness data continues to be surfaced on the UI so gaps remain visible.
|
||||
|
||||
let newProgress = request.progressPercentage;
|
||||
if (normalizedAction === 'APPROVE') {
|
||||
|
||||
@ -59,25 +59,69 @@ async function apiRequest(endpoint, method = 'GET', body = null, token = null) {
|
||||
return data;
|
||||
}
|
||||
|
||||
async function login(email) {
|
||||
async function login(email, password = PASSWORD) {
|
||||
if (!login.cache) login.cache = {};
|
||||
if (login.cache[email]) return login.cache[email];
|
||||
const data = await apiRequest('/auth/login', 'POST', { email, password: PASSWORD });
|
||||
const data = await apiRequest('/auth/login', 'POST', { email, password });
|
||||
login.cache[email] = data.token;
|
||||
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));
|
||||
|
||||
async function run() {
|
||||
try {
|
||||
console.log('--- STARTING CONSTITUTIONAL CHANGE E2E FLOW ---');
|
||||
if (!EMAILS.DEALER) {
|
||||
throw new Error('Missing --dealerEmail. This script requires an existing dealer user email.');
|
||||
}
|
||||
|
||||
console.log(`[STEP 0] Logging in as Dealer: ${EMAILS.DEALER}...`);
|
||||
const dealerToken = await login(EMAILS.DEALER);
|
||||
const adminTokenEarly = await login(EMAILS.DD_ADMIN);
|
||||
const discovered = await discoverDealer(adminTokenEarly, EMAILS.DEALER);
|
||||
EMAILS.DEALER = discovered.dealerEmail;
|
||||
const dealerToken = discovered.dealerToken;
|
||||
|
||||
console.log(`[STEP 0] Logged in as Dealer: ${EMAILS.DEALER}.`);
|
||||
|
||||
let requestId = args.requestId;
|
||||
if (!requestId) {
|
||||
@ -95,19 +139,7 @@ async function run() {
|
||||
console.log(`[STEP 1] Resuming request: ${requestId}`);
|
||||
}
|
||||
|
||||
// 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 adminToken = adminTokenEarly;
|
||||
const asmFromMapping = args.asmEmail || await resolveDealerAsmEmail(adminToken, EMAILS.DEALER);
|
||||
if (asmFromMapping) {
|
||||
EMAILS.ASM = asmFromMapping;
|
||||
@ -115,15 +147,44 @@ async function run() {
|
||||
} else {
|
||||
console.log(`[WARN] Dealer-level ASM not found. Falling back to default ASM email: ${EMAILS.ASM}`);
|
||||
}
|
||||
const current = await apiRequest(`/self-service/constitutional/${requestId}`, 'GET', null, adminToken);
|
||||
const currentStage = current?.request?.currentStage;
|
||||
const stageOrder = ['Submitted', 'ASM Review', 'ZM/RBM Review', 'ZBH Review', 'DD Lead Review', 'DD Head Review', 'NBH Approval', 'Legal Review', 'Completed'];
|
||||
const startIndex = Math.max(0, stageOrder.indexOf(currentStage));
|
||||
|
||||
let currentStep = 2 + startIndex;
|
||||
for (let i = startIndex; i < approvalSequence.length; i++) {
|
||||
const actor = approvalSequence[i];
|
||||
console.log(`[STEP ${currentStep}] ${actor.name} (${actor.email}) processing approval...`);
|
||||
// Stage → actor mapping mirrors constitutional.controller.ts stageRoleMap.
|
||||
// Driving the loop off the live currentStage (not a pre-computed index) keeps
|
||||
// the script self-healing if the workflow shape ever changes.
|
||||
const ACTOR_BY_STAGE = {
|
||||
'ASM Review': { name: 'ASM', email: EMAILS.ASM },
|
||||
'ZM/RBM Review': { name: 'ZM/RBM', email: EMAILS.RBM_L1 },
|
||||
'ZBH Review': { name: 'ZBH', email: EMAILS.ZBH },
|
||||
'DD Lead Review': { name: 'DD Lead', email: EMAILS.DD_LEAD },
|
||||
'DD Head Review': { name: 'DD Head', email: EMAILS.DD_HEAD },
|
||||
'NBH Approval': { name: 'NBH', email: EMAILS.NBH },
|
||||
'Legal Review': { name: 'Legal', email: EMAILS.LEGAL }
|
||||
};
|
||||
|
||||
let currentStep = 2;
|
||||
const MAX_ITER = 12; // safety cap
|
||||
for (let i = 0; i < MAX_ITER; i++) {
|
||||
const detailsRes = await apiRequest(`/self-service/constitutional/${requestId}`, 'GET', null, adminToken);
|
||||
const stage = detailsRes?.request?.currentStage;
|
||||
const status = detailsRes?.request?.status;
|
||||
|
||||
if (stage === 'Completed' || status === 'Completed') {
|
||||
console.log(`[STEP ${currentStep}] SUCCESS: Request reached COMPLETED state.`);
|
||||
break;
|
||||
}
|
||||
if (stage === 'Rejected' || status === 'Rejected' || stage === 'Revoked' || status === 'Revoked') {
|
||||
throw new Error(`Constitutional change ended in ${stage || status} state before completion.`);
|
||||
}
|
||||
|
||||
const actor = ACTOR_BY_STAGE[stage];
|
||||
if (!actor) {
|
||||
throw new Error(`No actor mapping found for stage: ${stage}`);
|
||||
}
|
||||
if (!actor.email) {
|
||||
throw new Error(`Missing approver email for stage ${stage} (actor ${actor.name}).`);
|
||||
}
|
||||
|
||||
console.log(`[STEP ${currentStep}] ${actor.name} (${actor.email}) processing ${stage}...`);
|
||||
const token = await login(actor.email);
|
||||
const res = await apiRequest(`/self-service/constitutional/${requestId}/action`, 'POST', {
|
||||
action: 'Approve',
|
||||
|
||||
@ -4,6 +4,7 @@ const args = Object.fromEntries(
|
||||
.map(([k, v]) => [k, v ?? 'true'])
|
||||
);
|
||||
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 STEP_DELAY_MS = Number(args.delayMs || 500);
|
||||
const SHOULD_SKIP_CLEARANCES = String(args.skipClearances || 'false') === 'true';
|
||||
|
||||
@ -312,18 +312,23 @@ async function run() {
|
||||
if (!terminationId) {
|
||||
log(1, 'Creating termination (ASM)...');
|
||||
const asmToken = await login(EMAILS.ASM);
|
||||
const createRes = await apiRequest(
|
||||
'/termination',
|
||||
'POST',
|
||||
{
|
||||
dealerId: targetDealer.id,
|
||||
category: args.category || 'Performance',
|
||||
reason: args.reason || 'Consistently failed to meet commitment targets.',
|
||||
proposedLwd: new Date().toISOString().split('T')[0],
|
||||
comments: 'E2E termination — follows UI stage order (no stacked partial approvals).'
|
||||
},
|
||||
asmToken
|
||||
);
|
||||
|
||||
// Backend (termination.controller.ts) requires at least one .ppt/.pptx
|
||||
// file for non-Super-Admin initiators, sent as multipart "files".
|
||||
const form = new FormData();
|
||||
form.append('dealerId', targetDealer.id);
|
||||
form.append('category', args.category || 'Performance');
|
||||
form.append('reason', args.reason || 'Consistently failed to meet commitment targets.');
|
||||
form.append('proposedLwd', new Date().toISOString().split('T')[0]);
|
||||
form.append('comments', 'E2E termination — follows UI stage order (no stacked partial approvals).');
|
||||
|
||||
const dummyPptxBytes = Buffer.from('E2E placeholder presentation');
|
||||
const dummyPptxBlob = new Blob([dummyPptxBytes], {
|
||||
type: 'application/vnd.openxmlformats-officedocument.presentationml.presentation'
|
||||
});
|
||||
form.append('files', dummyPptxBlob, 'e2e-termination-presentation.pptx');
|
||||
|
||||
const createRes = await apiRequest('/termination', 'POST', form, asmToken, true);
|
||||
terminationId = createRes.termination.id;
|
||||
log(1, `Created: ${terminationId} (${args.category || 'Performance'})`);
|
||||
} else {
|
||||
|
||||
@ -10,6 +10,7 @@ const args = Object.fromEntries(
|
||||
.map(([k, v]) => [k, v ?? 'true'])
|
||||
);
|
||||
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 OTP = '123456';
|
||||
const STEP_DELAY_MS = Number(args.delayMs || 1000);
|
||||
@ -381,204 +382,205 @@ async function triggerWorkflow() {
|
||||
log(7, 'FDD Milestone Complete.');
|
||||
await delay();
|
||||
|
||||
// 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'];
|
||||
// for (const doc of preLoiDocs) {
|
||||
// await mockUploadDocument(applicationUUID, adminToken, doc);
|
||||
// }
|
||||
// await delay(1000);
|
||||
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'];
|
||||
for (const doc of preLoiDocs) {
|
||||
await mockUploadDocument(applicationUUID, adminToken, doc);
|
||||
}
|
||||
await delay(1000);
|
||||
|
||||
// // 7.5 LOI APPROVAL (multi-approver: DD-Head + NBH)
|
||||
// log(7.5, 'Requesting LOI and collecting required approvals...');
|
||||
// const loiRes = await apiRequest('/loi/request', 'POST', { applicationId: applicationUUID }, adminToken);
|
||||
// const loiRequestId = loiRes.data.id;
|
||||
// log(7.5, `LOI Request created (ID: ${loiRequestId})`);
|
||||
// 7.5 LOI APPROVAL (multi-approver: DD-Head + NBH)
|
||||
log(7.5, 'Requesting LOI and collecting required approvals...');
|
||||
const loiRes = await apiRequest('/loi/request', 'POST', { applicationId: applicationUUID }, adminToken);
|
||||
const loiRequestId = loiRes.data.id;
|
||||
log(7.5, `LOI Request created (ID: ${loiRequestId})`);
|
||||
|
||||
// // DD-Head Approval
|
||||
// await apiRequest(`/loi/request/${loiRequestId}/approve`, 'POST', {
|
||||
// action: 'Approved',
|
||||
// remarks: 'DD-Head authorization for LOI'
|
||||
// }, headToken);
|
||||
// DD-Head Approval
|
||||
await apiRequest(`/loi/request/${loiRequestId}/approve`, 'POST', {
|
||||
action: 'Approved',
|
||||
remarks: 'DD-Head authorization for LOI'
|
||||
}, headToken);
|
||||
|
||||
// // NBH Approval — final approver flips overallStatus to "Security Details"
|
||||
// // (LOI Approved → moves into the Security Deposit corridor before LOI Issue).
|
||||
// await apiRequest(`/loi/request/${loiRequestId}/approve`, 'POST', {
|
||||
// action: 'Approved',
|
||||
// remarks: 'NBH authorization for LOI'
|
||||
// }, nbhToken);
|
||||
// NBH Approval — final approver flips overallStatus to "Security Details"
|
||||
// (LOI Approved → moves into the Security Deposit corridor before LOI Issue).
|
||||
await apiRequest(`/loi/request/${loiRequestId}/approve`, 'POST', {
|
||||
action: 'Approved',
|
||||
remarks: 'NBH authorization for LOI'
|
||||
}, nbhToken);
|
||||
|
||||
// log(7.5, 'LOI fully approved. Backend transitioned to Security Deposit corridor.');
|
||||
// await delay();
|
||||
log(7.5, 'LOI fully approved. Backend transitioned to Security Deposit corridor.');
|
||||
await delay();
|
||||
|
||||
// // 8. SECURITY DEPOSIT — Finance verifies advance payment.
|
||||
// // 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.
|
||||
// log(8, 'Finance Verifying SECURITY_DEPOSIT (₹5L advance)...');
|
||||
// const financeToken = await login(EMAILS.FINANCE);
|
||||
// await apiRequest('/loa/security-deposit', 'POST', {
|
||||
// applicationId: applicationUUID,
|
||||
// amount: 500000,
|
||||
// paymentReference: `PAY-SD-${Date.now()}`,
|
||||
// depositType: 'SECURITY_DEPOSIT',
|
||||
// status: 'Verified'
|
||||
// }, financeToken);
|
||||
// log(8, 'Security Deposit Verified.');
|
||||
// await delay();
|
||||
// 8. SECURITY DEPOSIT — Finance verifies advance payment.
|
||||
// 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.
|
||||
log(8, 'Finance Verifying SECURITY_DEPOSIT (₹5L advance)...');
|
||||
const financeToken = await login(EMAILS.FINANCE);
|
||||
await apiRequest('/loa/security-deposit', 'POST', {
|
||||
applicationId: applicationUUID,
|
||||
amount: 500000,
|
||||
paymentReference: `PAY-SD-${Date.now()}`,
|
||||
depositType: 'SECURITY_DEPOSIT',
|
||||
status: 'Verified'
|
||||
}, financeToken);
|
||||
log(8, 'Security Deposit Verified.');
|
||||
await delay();
|
||||
|
||||
// // 8.5 LOI DOCUMENTS COLLECTION
|
||||
// // 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".
|
||||
// log(8.5, 'Uploading LOI Documents (Letter of Intent + Signed LOI) prior to LOI Issuance...');
|
||||
// const loiDocs = ['Letter of Intent', 'Signed LOI'];
|
||||
// for (const doc of loiDocs) {
|
||||
// await mockUploadDocument(applicationUUID, adminToken, doc);
|
||||
// }
|
||||
// log(8.5, 'LOI Documents uploaded. Ready for LOI Issued transition.');
|
||||
// await delay();
|
||||
// 8.5 LOI DOCUMENTS COLLECTION
|
||||
// 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".
|
||||
log(8.5, 'Uploading LOI Documents (Letter of Intent + Signed LOI) prior to LOI Issuance...');
|
||||
const loiDocs = ['Letter of Intent', 'Signed LOI'];
|
||||
for (const doc of loiDocs) {
|
||||
await mockUploadDocument(applicationUUID, adminToken, doc);
|
||||
}
|
||||
log(8.5, 'LOI Documents uploaded. Ready for LOI Issued transition.');
|
||||
await delay();
|
||||
|
||||
// // 9. LOI ISSUE — explicit admin transition + LOI document generation.
|
||||
// let statusBeforeCodeGen = await getApplicationStatus(applicationUUID, adminToken);
|
||||
// log(9, `Current status before LOI Issued transition: ${statusBeforeCodeGen}`);
|
||||
// 9. LOI ISSUE — explicit admin transition + LOI document generation.
|
||||
let statusBeforeCodeGen = await getApplicationStatus(applicationUUID, adminToken);
|
||||
log(9, `Current status before LOI Issued transition: ${statusBeforeCodeGen}`);
|
||||
|
||||
// log(9, 'Ensuring mandatory PAN/GST/Bank fields are populated...');
|
||||
// await ensureMandatoryCodeGenFields(applicationUUID, adminToken);
|
||||
// await delay(300);
|
||||
log(9, 'Ensuring mandatory PAN/GST/Bank fields are populated...');
|
||||
await ensureMandatoryCodeGenFields(applicationUUID, adminToken);
|
||||
await delay(300);
|
||||
|
||||
// if (statusBeforeCodeGen === 'Security Deposit' || statusBeforeCodeGen === 'Security Details') {
|
||||
// log(9, 'Applying admin transition: Security Deposit -> LOI Issued...');
|
||||
// await apiRequest(`/onboarding/applications/${applicationUUID}/status`, 'PUT', {
|
||||
// status: 'LOI Issued',
|
||||
// stage: 'LOI',
|
||||
// reason: 'LOI documents collected and verified. Releasing LOI Issued milestone.'
|
||||
// }, adminToken);
|
||||
// await delay();
|
||||
// statusBeforeCodeGen = await getApplicationStatus(applicationUUID, adminToken);
|
||||
// log(9, `Status after admin transition: ${statusBeforeCodeGen}`);
|
||||
// }
|
||||
if (statusBeforeCodeGen === 'Security Deposit' || statusBeforeCodeGen === 'Security Details') {
|
||||
log(9, 'Applying admin transition: Security Deposit -> LOI Issued...');
|
||||
await apiRequest(`/onboarding/applications/${applicationUUID}/status`, 'PUT', {
|
||||
status: 'LOI Issued',
|
||||
stage: 'LOI',
|
||||
reason: 'LOI documents collected and verified. Releasing LOI Issued milestone.'
|
||||
}, adminToken);
|
||||
await delay();
|
||||
statusBeforeCodeGen = await getApplicationStatus(applicationUUID, adminToken);
|
||||
log(9, `Status after admin transition: ${statusBeforeCodeGen}`);
|
||||
}
|
||||
|
||||
// // generateDocument bridges LOI Issued -> Dealer Code Generation (loi.controller.ts:459).
|
||||
// log(9, 'Generating final LOI document (bridges to Dealer Code Generation)...');
|
||||
// await apiRequest('/loi/generate-document', 'POST', { requestId: loiRequestId }, adminToken);
|
||||
// await delay();
|
||||
// statusBeforeCodeGen = await getApplicationStatus(applicationUUID, adminToken);
|
||||
// log(9, `Status after LOI generateDocument: ${statusBeforeCodeGen}`);
|
||||
// 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)...');
|
||||
await apiRequest(`/loi/request/${loiRequestId}/generate`, 'POST', {}, adminToken);
|
||||
await delay();
|
||||
statusBeforeCodeGen = await getApplicationStatus(applicationUUID, adminToken);
|
||||
log(9, `Status after LOI generateDocument: ${statusBeforeCodeGen}`);
|
||||
|
||||
// if (statusBeforeCodeGen !== 'LOI Issued' && statusBeforeCodeGen !== 'Dealer Code Generation') {
|
||||
// throw new Error(`Cannot generate codes: expected LOI Issued/Dealer Code Generation, got ${statusBeforeCodeGen}`);
|
||||
// }
|
||||
if (statusBeforeCodeGen !== 'LOI Issued' && statusBeforeCodeGen !== 'Dealer Code Generation') {
|
||||
throw new Error(`Cannot generate codes: expected LOI Issued/Dealer Code Generation, got ${statusBeforeCodeGen}`);
|
||||
}
|
||||
|
||||
// log(9, 'Admin Generating SAP Dealer Codes...');
|
||||
// await apiRequest(`/onboarding/applications/${applicationUUID}/generate-codes`, 'POST', {}, adminToken);
|
||||
// log(9, 'Dealer Codes Generated.');
|
||||
// await delay();
|
||||
log(9, 'Admin Generating SAP Dealer Codes...');
|
||||
await apiRequest(`/onboarding/applications/${applicationUUID}/generate-codes`, 'POST', {}, adminToken);
|
||||
log(9, 'Dealer Codes Generated.');
|
||||
await delay();
|
||||
|
||||
// // 10. FIRST FILL (POST CODE-GENERATION)
|
||||
// log(10, 'Finance Verifying FIRST FILL (₹15L)...');
|
||||
// await apiRequest('/loa/security-deposit', 'POST', {
|
||||
// applicationId: applicationUUID,
|
||||
// amount: 1500000,
|
||||
// paymentReference: `PAY-FF-${Date.now()}`,
|
||||
// depositType: 'FIRST_FILL',
|
||||
// status: 'Verified'
|
||||
// }, financeToken);
|
||||
// log(10, 'First Fill Verified.');
|
||||
// await delay();
|
||||
// 10. FIRST FILL (POST CODE-GENERATION)
|
||||
log(10, 'Finance Verifying FIRST FILL (₹15L)...');
|
||||
await apiRequest('/loa/security-deposit', 'POST', {
|
||||
applicationId: applicationUUID,
|
||||
amount: 1500000,
|
||||
paymentReference: `PAY-FF-${Date.now()}`,
|
||||
depositType: 'FIRST_FILL',
|
||||
status: 'Verified'
|
||||
}, financeToken);
|
||||
log(10, 'First Fill Verified.');
|
||||
await delay();
|
||||
|
||||
// // 11. ADMIN UPDATING STATUTORY & BANK DETAILS
|
||||
// log(11, 'Admin Updating Statutory & Bank Details for LOA Approval Gate...');
|
||||
// await apiRequest(`/onboarding/applications/${applicationUUID}`, 'PUT', {
|
||||
// accountHolderName: 'Ramesh Automobiles Private Limited',
|
||||
// panNumber: 'ABCDE1234F',
|
||||
// gstNumber: '07ABCDE1234F1Z5',
|
||||
// bankName: 'HDFC Bank',
|
||||
// accountNumber: '50100223344556',
|
||||
// ifscCode: 'HDFC0001234'
|
||||
// }, adminToken);
|
||||
// log(11, 'Statutory & Bank details updated.');
|
||||
// await delay();
|
||||
// 11. ADMIN UPDATING STATUTORY & BANK DETAILS
|
||||
log(11, 'Admin Updating Statutory & Bank Details for LOA Approval Gate...');
|
||||
await apiRequest(`/onboarding/applications/${applicationUUID}`, 'PUT', {
|
||||
accountHolderName: 'Ramesh Automobiles Private Limited',
|
||||
panNumber: 'ABCDE1234F',
|
||||
gstNumber: '07ABCDE1234F1Z5',
|
||||
bankName: 'HDFC Bank',
|
||||
accountNumber: '50100223344556',
|
||||
ifscCode: 'HDFC0001234'
|
||||
}, adminToken);
|
||||
log(11, 'Statutory & Bank details updated.');
|
||||
await delay();
|
||||
|
||||
// // 12. FINAL LOA APPROVAL
|
||||
// log(12, 'NBH & Head Approving Final LOA...');
|
||||
// const loaRes = await apiRequest('/loa/request', 'POST', { applicationId: applicationUUID }, headToken);
|
||||
// const finalLoaRequestId = loaRes.data.id;
|
||||
// 12. FINAL LOA APPROVAL
|
||||
log(12, 'NBH & Head Approving Final LOA...');
|
||||
const loaRes = await apiRequest('/loa/request', 'POST', { applicationId: applicationUUID }, headToken);
|
||||
const finalLoaRequestId = loaRes.data.id;
|
||||
|
||||
// await apiRequest(`/loa/request/${finalLoaRequestId}/approve`, 'POST', {
|
||||
// action: 'Approved',
|
||||
// remarks: 'Head Authorization (Level 1)'
|
||||
// }, headToken);
|
||||
await apiRequest(`/loa/request/${finalLoaRequestId}/approve`, 'POST', {
|
||||
action: 'Approved',
|
||||
remarks: 'Head Authorization (Level 1)'
|
||||
}, headToken);
|
||||
|
||||
// await apiRequest(`/loa/request/${finalLoaRequestId}/approve`, 'POST', {
|
||||
// action: 'Approved',
|
||||
// remarks: 'NBH Approval (Level 2)'
|
||||
// }, nbhToken);
|
||||
// log(12, 'LOA Fully Approved.');
|
||||
// await delay();
|
||||
await apiRequest(`/loa/request/${finalLoaRequestId}/approve`, 'POST', {
|
||||
action: 'Approved',
|
||||
remarks: 'NBH Approval (Level 2)'
|
||||
}, nbhToken);
|
||||
log(12, 'LOA Fully Approved.');
|
||||
await delay();
|
||||
|
||||
// // 13. EOR (EVIDENCE OF READINESS) CHECKLIST VERIFICATION
|
||||
// log(13, 'Admin Initializing EOR Checklist (100% Readiness Requirement)...');
|
||||
// const eorInit = await apiRequest('/eor', 'POST', { applicationId: applicationUUID }, adminToken);
|
||||
// const checklistId = eorInit.data.id;
|
||||
// log(13, `EOR Checklist Created (ID: ${checklistId})`);
|
||||
// 13. EOR (EVIDENCE OF READINESS) CHECKLIST VERIFICATION
|
||||
log(13, 'Admin Initializing EOR Checklist (100% Readiness Requirement)...');
|
||||
const eorInit = await apiRequest('/eor', 'POST', { applicationId: applicationUUID }, adminToken);
|
||||
const checklistId = eorInit.data.id;
|
||||
log(13, `EOR Checklist Created (ID: ${checklistId})`);
|
||||
|
||||
// log(13.1, 'Auditor Verifying all 12 mandatory EOR items as COMPLIANT...');
|
||||
// const eorItems = [
|
||||
// { itemType: 'Sales', description: 'Sales Standards' },
|
||||
// { itemType: 'Service', description: 'Service & Spares' },
|
||||
// { itemType: 'IT', description: 'DMS infra' },
|
||||
// { itemType: 'Training', description: 'Manpower Training' },
|
||||
// { itemType: 'Statutory', description: 'Trade certificate with test ride bikes registration' },
|
||||
// { itemType: 'Statutory', description: 'GST certificate including Accessories & Apparels billing' },
|
||||
// { itemType: 'Finance', description: 'Inventory Funding' },
|
||||
// { itemType: 'IT', description: 'Virtual code availability' },
|
||||
// { itemType: 'Finance', description: 'Vendor payments' },
|
||||
// { itemType: 'Marketing', description: 'Details for website submission' },
|
||||
// { itemType: 'Insurance', description: 'Infra Insurance both Showroom and Service center' },
|
||||
// { itemType: 'IT', description: 'Auto ordering' }
|
||||
// ];
|
||||
log(13.1, 'Auditor Verifying all 12 mandatory EOR items as COMPLIANT...');
|
||||
const eorItems = [
|
||||
{ itemType: 'Sales', description: 'Sales Standards' },
|
||||
{ itemType: 'Service', description: 'Service & Spares' },
|
||||
{ itemType: 'IT', description: 'DMS infra' },
|
||||
{ itemType: 'Training', description: 'Manpower Training' },
|
||||
{ itemType: 'Statutory', description: 'Trade certificate with test ride bikes registration' },
|
||||
{ itemType: 'Statutory', description: 'GST certificate including Accessories & Apparels billing' },
|
||||
{ itemType: 'Finance', description: 'Inventory Funding' },
|
||||
{ itemType: 'IT', description: 'Virtual code availability' },
|
||||
{ itemType: 'Finance', description: 'Vendor payments' },
|
||||
{ itemType: 'Marketing', description: 'Details for website submission' },
|
||||
{ itemType: 'Insurance', description: 'Infra Insurance both Showroom and Service center' },
|
||||
{ itemType: 'IT', description: 'Auto ordering' }
|
||||
];
|
||||
|
||||
// for (const item of eorItems) {
|
||||
// process.stdout.write(`.`);
|
||||
// await apiRequest(`/eor/item/${checklistId}`, 'POST', {
|
||||
// ...item,
|
||||
// isCompliant: true,
|
||||
// remarks: 'Verified by Auditor - Compliant'
|
||||
// }, adminToken);
|
||||
// }
|
||||
// console.log('\n[STEP 13.1] All EOR items marked as compliant.');
|
||||
for (const item of eorItems) {
|
||||
process.stdout.write(`.`);
|
||||
await apiRequest(`/eor/item/${checklistId}`, 'POST', {
|
||||
...item,
|
||||
isCompliant: true,
|
||||
remarks: 'Verified by Auditor - Compliant'
|
||||
}, adminToken);
|
||||
}
|
||||
console.log('\n[STEP 13.1] All EOR items marked as compliant.');
|
||||
|
||||
// log(13.2, 'Auditor Submitting Final EOR Audit...');
|
||||
// await apiRequest(`/eor/audit/${checklistId}`, 'POST', {
|
||||
// status: 'Completed',
|
||||
// overallComments: 'Dealer is 100% ready for inauguration. All infra and statutory items verified.'
|
||||
// }, adminToken);
|
||||
log(13.2, 'Auditor Submitting Final EOR Audit...');
|
||||
await apiRequest(`/eor/audit/${checklistId}`, 'POST', {
|
||||
status: 'Completed',
|
||||
overallComments: 'Dealer is 100% ready for inauguration. All infra and statutory items verified.'
|
||||
}, adminToken);
|
||||
|
||||
// const finalAppStatus = await apiRequest(`/onboarding/applications/${applicationUUID}`, 'GET', null, adminToken);
|
||||
// log(13.2, `Application Status after EOR: ${finalAppStatus.data.overallStatus}`);
|
||||
// await delay();
|
||||
const finalAppStatus = await apiRequest(`/onboarding/applications/${applicationUUID}`, 'GET', null, adminToken);
|
||||
log(13.2, `Application Status after EOR: ${finalAppStatus.data.overallStatus}`);
|
||||
await delay();
|
||||
|
||||
// // 14. FINAL ONBOARDING
|
||||
// log(14, 'Admin Finalizing Dealer Onboarding...');
|
||||
// await apiRequest('/dealers', 'POST', { applicationId: applicationUUID }, adminToken);
|
||||
// await delay();
|
||||
// 14. FINAL ONBOARDING
|
||||
log(14, 'Admin Finalizing Dealer Onboarding...');
|
||||
await apiRequest('/dealers', 'POST', { applicationId: applicationUUID }, adminToken);
|
||||
await delay();
|
||||
|
||||
// // 15. VERIFICATION
|
||||
// log(15, 'Verifying Dealer Record Creation...');
|
||||
// const dealerRes = await apiRequest(`/dealers/application/${applicationUUID}`, 'GET', null, adminToken);
|
||||
// if (!dealerRes.success || !dealerRes.data) {
|
||||
// throw new Error('Verification Failed: Dealer record not found after onboarding.');
|
||||
// }
|
||||
// log(15, `Dealer Found: ${dealerRes.data.legalName} (${dealerRes.data.id})`);
|
||||
// 15. VERIFICATION
|
||||
log(15, 'Verifying Dealer Record Creation...');
|
||||
const dealerRes = await apiRequest(`/dealers/application/${applicationUUID}`, 'GET', null, adminToken);
|
||||
if (!dealerRes.success || !dealerRes.data) {
|
||||
throw new Error('Verification Failed: Dealer record not found after onboarding.');
|
||||
}
|
||||
log(15, `Dealer Found: ${dealerRes.data.legalName} (${dealerRes.data.id})`);
|
||||
|
||||
// log(15.1, 'Verifying User Account Role Update...');
|
||||
// const userRes = await apiRequest(`/admin/users`, 'GET', null, adminToken);
|
||||
// const dealerUser = userRes.data.find(u => u.email === PROSPECT_EMAIL);
|
||||
// if (!dealerUser || dealerUser.roleCode !== 'Dealer') {
|
||||
// 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, 'Verifying User Account Role Update...');
|
||||
const userRes = await apiRequest(`/admin/users`, 'GET', null, adminToken);
|
||||
const dealerUser = userRes.data.find(u => u.email === PROSPECT_EMAIL);
|
||||
if (!dealerUser || dealerUser.roleCode !== 'Dealer') {
|
||||
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.2, '--- WORKFLOW COMPLETED SUCCESSFULLY! ---');
|
||||
// log(15.2, `The application ${applicationId} is now at 'ONBOARDED' status and Dealer profile is active.`);
|
||||
log(15.2, '--- WORKFLOW COMPLETED SUCCESSFULLY! ---');
|
||||
log(15.2, `The application ${applicationId} is now at 'ONBOARDED' status and Dealer profile is active.`);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Loading…
Reference in New Issue
Block a user