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 SHOULD_SKIP_CLEARANCES = String(args.skipClearances || 'false') === 'true'; const SHOULD_SKIP_FINAL_SETTLEMENT = String(args.skipSettlement || 'false') === 'true'; const EMAILS = { DD_ADMIN: 'lince@royalenfield.com', DEALER: args.dealerEmail, ASM: 'abhishek@royalenfield.com', RBM: 'manish@royalenfield.com', ZBH: 'manav@royalenfield.com', DD_LEAD: 'jaya@royalenfield.com', NBH: 'yashwin@royalenfield.com', LEGAL: 'legal@royalenfield.com', FINANCE: 'finance@royalenfield.com', SALES: 'sales@royalenfield.com', SERVICE: 'service@royalenfield.com', SPARES: 'spares@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 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) { if (!login.cache) login.cache = {}; if (login.cache[email]) return login.cache[email]; const isInternal = email.endsWith('@royalenfield.com'); const password = isInternal ? 'Admin@123' : 'Dealer@123'; const data = await apiRequest('/auth/login', 'POST', { email, password }); login.cache[email] = data.token; return login.cache[email]; } const delay = (ms = STEP_DELAY_MS) => new Promise(res => setTimeout(res, ms)); const log = (step, msg) => console.log(`[STEP ${step}] ${msg}`); async function run() { try { console.log('--- STARTING DEALER RESIGNATION E2E FLOW ---'); const adminToken = await login(EMAILS.DD_ADMIN); const appsRes = await apiRequest('/onboarding/applications', 'GET', null, adminToken); const onboardedApps = appsRes.data.filter(a => (a.overallStatus || a.status || '').toLowerCase() === 'onboarded'); if (onboardedApps.length === 0) throw new Error('No onboarded applications found for resignation test.'); // 1.0 Find an active Dealer and Login let targetApp = null; let dealerToken = null; for (const app of onboardedApps) { try { process.stdout.write(`Testing login for ${app.email}... `); const dealerData = await apiRequest('/auth/login', 'POST', { email: app.email, password: 'Dealer@123' }); dealerToken = dealerData.token; targetApp = app; console.log('SUCCESS'); break; } catch (e) { console.log('FAILED (Deactivated)'); } } if (!targetApp) throw new Error('All onboarded applications are deactivated. Run onboarding first.'); console.log(`Targeting Application: ${targetApp.applicantName} (${targetApp.id}) - Email: ${targetApp.email}`); await delay(); // 1.1 Discover Dealer's Outlet console.log(`[STEP 1.1] Discovering Outlets for Dealer...`); const dealerDashboard = await apiRequest('/dealer/dashboard', 'GET', null, dealerToken); const targetOutlet = dealerDashboard.data.outlets[0]; await delay(); if (!targetOutlet) throw new Error('No outlets found for this dealer. Ensure they are fully onboarded.'); console.log(`Found Target Outlet: ${targetOutlet.name} (${targetOutlet.code})`); console.log(`[STEP 1.2] Dealer Submitting Resignation for Outlet...`); let resignationId = args.resignationId; if (!resignationId) { try { const createRes = await apiRequest('/self-service/resignations', 'POST', { outletId: targetOutlet.id, resignationType: 'Voluntary', lastOperationalDateSales: new Date().toISOString().split('T')[0], lastOperationalDateServices: new Date().toISOString().split('T')[0], reason: 'Focusing on other business ventures', remarks: 'Initiating voluntary resignation for E2E validation.' }, dealerToken); resignationId = createRes.resignation.id; log(1, `Resignation Created. ID: ${resignationId}`); } catch (e) { if (e.message.includes('already has an active resignation request')) { console.log(`[STEP 1.2] Active resignation already exists. Fetching...`); const activeResRes = await apiRequest('/self-service/resignations', 'GET', null, dealerToken); const activeRes = (activeResRes.resignations || activeResRes.data).find(r => r.outletId === targetOutlet.id && !['Completed', 'Rejected'].includes(r.status)); resignationId = activeRes.id; log(1, `Resuming with existing Resignation: ${resignationId}`); } else { throw e; } } } else { log(1, `Resuming provided resignation: ${resignationId}`); } await delay(); const approvals = [ { name: 'ASM', email: EMAILS.ASM, remarks: 'Verified physical assets and dealer intent. Recommended for resignation.' }, { name: 'RBM', email: EMAILS.RBM, remarks: 'No active sales pipeline issues in the territory. Transition plan discussed.' }, { name: 'ZBH', email: EMAILS.ZBH, remarks: 'Zone-level resource reallocation planned. Approved.' }, { name: 'DD Lead', email: EMAILS.DD_LEAD, remarks: 'Dealer development criteria satisfied for exit.' }, { name: 'NBH', email: EMAILS.NBH, remarks: 'HQ clearance granted. Proceed with F&F initiation.' }, { name: 'DD Admin', email: EMAILS.DD_ADMIN, remarks: 'Administrative checks complete. Initiating termination of commercial contract.' }, { name: 'Legal Admin', email: EMAILS.LEGAL, remarks: 'Legal documentation verified. No active litigation found.' } ]; // Fetch resignation data to determine current stage for skipping const resignationData = await apiRequest(`/self-service/resignations/${resignationId}`, 'GET', null, adminToken); const currentStage = resignationData.resignation.currentStage; console.log(`Current Stage: ${currentStage}`); const stageOrder = [ 'ASM', 'RBM', 'ZBH', 'DD Lead', 'NBH', 'DD Admin', 'Legal Admin', 'F&F Initiated', 'Completed' ]; const startIndex = stageOrder.indexOf(currentStage) === -1 ? 0 : stageOrder.indexOf(currentStage); let currentStep = 2; for (let i = startIndex; i < approvals.length; i++) { const actor = approvals[i]; log(currentStep, `${actor.name} (${actor.email}) approving...`); const token = await login(actor.email); const res = await apiRequest(`/self-service/resignations/${resignationId}/approve`, 'PUT', { remarks: actor.remarks, force: true }, token); log(currentStep, `${actor.name} Result: ${res.message || 'SUCCESS'}`); currentStep++; await delay(); } // --- F&F CLEARANCE LOOP (must match backend FNF_DEPARTMENTS + resignation updateClearance) --- // F&F is created with no default line items; this loop is what creates DepartmentClaim / receivable & payable rows. // Sends JSON PUT (no file). Backend uses uploadSingleIfMultipart so express.json body is preserved. // type: 'Receivable' | 'Payable' (backend normalizes legacy 'Recovery' to Receivable). if (!SHOULD_SKIP_CLEARANCES) { console.log('[STEP 9] Starting 16-Department F&F Clearance Flow...'); } // Re-fetch to ensure we have the F&F ID regardless of start point const finalResData = await apiRequest(`/self-service/resignations/${resignationId}`, 'GET', null, adminToken); const fnfId = finalResData.resignation.settlement?.id; if (!fnfId) { throw new Error(`F&F Settlement ID not found for Resignation ${resignationId}. Ensure it reached F&F Initiation stage.`); } console.log(`F&F Settlement ID: ${fnfId}`); await delay(); const departments = [ { name: 'Warranty Department', status: 'Cleared', amount: 0, type: 'Receivable', remarks: 'No pending claims.' }, { name: 'Accessories Department', status: 'Cleared', amount: 0, type: 'Receivable', remarks: 'Stock returned and verified.' }, { name: 'Sales Department', status: 'Cleared', amount: 0, type: 'Receivable', remarks: 'Allocations transferred.' }, { name: 'RTO Department', status: 'Dues', amount: 1500, type: 'Receivable', remarks: 'Pending RTO tax recovery.' }, { name: 'Service Department', status: 'Cleared', amount: 0, type: 'Receivable', remarks: 'Service tools handed over.' }, { name: 'Parts Department', status: 'Dues', amount: 45000, type: 'Payable', remarks: 'Parts credit note adjustment.' }, { name: 'Finance Department', status: 'Dues', amount: 25000, type: 'Receivable', remarks: 'Short-term loan interest.' }, { name: 'Insurance Department', status: 'Cleared', amount: 0, type: 'Receivable', remarks: 'Policy renewals handled.' }, { name: 'Inventory Department', status: 'Cleared', amount: 0, type: 'Receivable', remarks: 'Physical inventory reconciled.' }, { name: 'Marketing Department', status: 'Dues', amount: 5000, type: 'Receivable', remarks: 'Glow-sign board removal cost.' }, { name: 'HR Department', status: 'Cleared', amount: 0, type: 'Receivable', remarks: 'Dealer staff settlement verified.' }, { name: 'IT Department', status: 'Dues', amount: 12000, type: 'Receivable', remarks: 'Laptop / DMS hardware dues.' }, { name: 'Legal Department', status: 'Cleared', amount: 0, type: 'Receivable', remarks: 'Legal NOC issued.' }, { name: 'Quality Department', status: 'Cleared', amount: 0, type: 'Receivable', remarks: 'Quality audit passed.' }, { name: 'Logistics Department', status: 'Cleared', amount: 0, type: 'Receivable', remarks: 'Last vehicle transit clear.' }, { name: 'Customer Relations Department', status: 'Cleared', amount: 0, type: 'Receivable', remarks: 'Customer complaints resolved.' } ]; if (!SHOULD_SKIP_CLEARANCES) { for (const dept of departments) { log('9.1', `Clearing Dept: ${dept.name} [${dept.status}] - ${dept.remarks}`); await apiRequest(`/self-service/resignations/${resignationId}/clearance`, 'PUT', { department: dept.name, status: dept.status, remarks: dept.remarks, amount: dept.amount, type: dept.type }, adminToken); await delay(100); } log(9, 'All 16 Departments Cleared.'); await delay(); } // --- FINAL FINANCE SETTLEMENT --- if (!SHOULD_SKIP_FINAL_SETTLEMENT) { console.log('[STEP 10] Finance Finalizing Settlement...'); const financeToken = await login(EMAILS.FINANCE); await apiRequest(`/settlement/fnf/${fnfId}`, 'PUT', { status: 'Completed', finalSettlementAmount: Number(args.finalSettlementAmount || 415173), paymentMode: 'NEFT / Bank Transfer', transactionReference: `TXN-${Date.now()}`, settlementDate: new Date().toISOString(), remarks: 'Settlement completed and verified via automated script.' }, financeToken); await delay(); } // --- FINAL COMPLETION --- console.log('[STEP 11] Verifying Resignation is now COMPLETED (Auto-transitioned)...'); const finalStatusRes = await apiRequest(`/self-service/resignations/${resignationId}`, 'GET', null, adminToken); if (finalStatusRes.resignation.status === 'Completed') { log(11, 'Resignation auto-transitioned to Completed successfully.'); } else { console.log(`[STEP 11] FAILED: Current status is ${finalStatusRes.resignation.status}`); // Fallback: manually trigger completion if auto-sync failed to keep script running await apiRequest(`/self-service/resignations/${resignationId}/approve`, 'PUT', { remarks: 'Final resignation completion (Manual Fallback).', force: true }, adminToken); } await delay(); // [FINAL STEP] Verification of deactivation console.log(`[FINAL STEP] Verifying Account Deactivation...`); // Get updated user status const userRes = await apiRequest('/admin/users', 'GET', null, adminToken); // Fetch dealer to get its associated user ID const dealerU = userRes.data.find(u => u.email === targetApp.email); if (dealerU && (dealerU.status === 'deactivated' || !dealerU.isActive)) { console.log(`[VERIFICATION] SUCCESS: Account ${dealerU.email} is deactivated. Status: ${dealerU.status}`); } else { console.log(`[VERIFICATION] Failed: Account ${targetApp.email} is still active. Status: ${dealerU?.status || 'unknown'}`); throw new Error('Automated account deactivation check failed.'); } console.log('--- DEALER RESIGNATION E2E FLOW COMPLETED SUCCESSFULLY ---'); } catch (error) { console.error('Workflow failed:', error.message); process.exit(1); } } run();