Dealer_Onboarding_Backend/trigger-constitutional.js

219 lines
9.4 KiB
JavaScript

const args = Object.fromEntries(
process.argv.slice(2)
.map(arg => arg.replace(/^--/, '').split('='))
.map(([k, v]) => [k, v ?? 'true'])
);
const BASE_URL = args.baseUrl || process.env.BASE_URL || 'http://localhost:5000/api';
const PASSWORD = 'Admin@123';
const STEP_DELAY_MS = Number(args.delayMs || 500);
const EMAILS = {
DD_ADMIN: 'lince@royalenfield.com',
DEALER: args.dealerEmail,
ASM: args.asmEmail || 'abhishek@royalenfield.com',
RBM_L1: 'manish@royalenfield.com',
ZBH: 'manav@royalenfield.com',
DD_LEAD: 'jaya@royalenfield.com',
DD_HEAD: 'ganesh@royalenfield.com',
NBH: 'yashwin@royalenfield.com',
LEGAL: 'legal@royalenfield.com',
SALES: 'sales@royalenfield.com',
SERVICE: 'service@royalenfield.com',
SPARES: 'spares@royalenfield.com',
FINANCE: 'finance@royalenfield.com',
ACCOUNTS: 'accounts@royalenfield.com',
WARRANTY: 'warranty@royalenfield.com',
MARKETING: 'marketing@royalenfield.com',
HR: 'hr@royalenfield.com',
IT: 'it@royalenfield.com',
LOGISTICS: 'logistics@royalenfield.com',
QUALITY: 'quality@royalenfield.com',
APPAREL: 'apparel@royalenfield.com',
DMS: 'dms@royalenfield.com'
};
async function resolveDealerAsmEmail(adminToken, dealerEmail) {
try {
const res = await apiRequest('/master/dealer-asm-mappings', 'GET', null, adminToken);
const rows = res?.data?.dealers || [];
const match = rows.find((d) => String(d?.dealerUser?.email || '').toLowerCase() === String(dealerEmail || '').toLowerCase());
return match?.assignedAsm?.email || null;
} catch (e) {
return null;
}
}
async function apiRequest(endpoint, method = 'GET', body = null, token = null) {
const headers = { 'Content-Type': 'application/json' };
if (token) headers['Authorization'] = `Bearer ${token}`;
const config = { method, headers };
if (body) config.body = JSON.stringify(body);
const response = await fetch(`${BASE_URL}${endpoint}`, config);
const data = await response.json();
if (!response.ok) {
throw new Error(`API Error ${method} ${endpoint}: ${JSON.stringify(data)}`);
}
return data;
}
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 });
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 ---');
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) {
console.log('[STEP 1] Dealer Submitting Constitutional Change...');
const createRes = await apiRequest('/self-service/constitutional', 'POST', {
changeType: args.changeType || 'LLP Conversion',
reason: args.reason || 'Converting to LLP for better operational governance.',
currentConstitution: 'Proprietorship',
newPartnersDetails: 'John Doe, Jane Smith',
shareholdingPattern: '60/40'
}, dealerToken);
requestId = createRes.requestId;
console.log(`[STEP 1] Request Created. RequestID: ${requestId}`);
} else {
console.log(`[STEP 1] Resuming request: ${requestId}`);
}
const adminToken = adminTokenEarly;
const asmFromMapping = args.asmEmail || await resolveDealerAsmEmail(adminToken, EMAILS.DEALER);
if (asmFromMapping) {
EMAILS.ASM = asmFromMapping;
console.log(`[INFO] Using ASM approver: ${EMAILS.ASM}`);
} else {
console.log(`[WARN] Dealer-level ASM not found. Falling back to default ASM email: ${EMAILS.ASM}`);
}
// 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',
comments: `${actor.name} verified the request.`
}, token);
console.log(`[STEP ${currentStep}] ${actor.name} Result: ${res.message}`);
currentStep++;
await delay(500);
}
console.log('[FINAL STEP] Verifying Completion Status...');
const finalDetails = await apiRequest(`/self-service/constitutional/${requestId}`, 'GET', null, adminToken);
if (finalDetails.request.status === 'Completed' || finalDetails.request.currentStage === 'Completed') {
console.log(`[STEP ${currentStep}] SUCCESS: Request reached COMPLETED state.`);
} else {
console.error(`Verification Failed: Final stage is ${finalDetails.request.currentStage}. Status: ${finalDetails.request.status}`);
throw new Error('Constitutional change completion check failed.');
}
console.log('\n--- VERIFICATION RESULTS ---');
console.log('Outcome: CONSTITUTIONAL CHANGE FLOW COMPLETED SUCCESSFULLY');
process.exit(0);
} catch (error) {
console.error('Workflow failed:', error.message);
process.exit(1);
}
}
run();