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();