import { test, expect } from '@playwright/test'; import { login } from './helpers/auth'; import { epic, feature, severity, description, tag } from './helpers/allure'; import { navigateToSettlement, navigateToTransactions } from './helpers/navigation'; import * as fs from 'fs'; import * as path from 'path'; import * as XLSX from 'xlsx'; // Parses either an ISO picker-cell title (YYYY-MM-DD) or a display-format // table date ("Jul 6, 2026") into a comparable Date. const toDate = (s: string | null | undefined): Date | null => { if (!s) return null; const iso = s.match(/(\d{4})-(\d{2})-(\d{2})/); if (iso) return new Date(`${iso[1]}-${iso[2]}-${iso[3]}T00:00:00`); const disp = s.match(/(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)\s+(\d{1,2}),\s*(\d{4})/); if (disp) { const months: Record = { Jan: '01', Feb: '02', Mar: '03', Apr: '04', May: '05', Jun: '06', Jul: '07', Aug: '08', Sep: '09', Oct: '10', Nov: '11', Dec: '12', }; return new Date(`${disp[3]}-${months[disp[1]]}-${disp[2].padStart(2, '0')}T00:00:00`); } return null; }; const toISO = (d: Date): string => `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`; // Navigates the calendar backward (bounded) until the target ISO date is // visible, then clicks it. The picker defaults to the current month, which // may be well after this dev dataset's latest transaction. const selectCalendarDate = async (page: import('@playwright/test').Page, targetIso: string, prevBtnSelector: string) => { const prevBtn = page.locator(prevBtnSelector).first(); let found = false; for (let attempt = 0; attempt < 60; attempt++) { found = await page.locator(`.ant-picker-cell-in-view[title="${targetIso}"]:not(.ant-picker-cell-disabled)`).first().isVisible().catch(() => false); if (found) break; await prevBtn.click(); await page.waitForTimeout(300); } expect(found, `Target date ${targetIso} should become available in the calendar`).toBe(true); await page.locator(`.ant-picker-cell-in-view[title="${targetIso}"]`).first().click(); }; test.describe('Settlement Tests', () => { test.setTimeout(120000); test.beforeEach(async ({ page }) => { epic('Settlement Management'); await login(page); }); test('should download transactions, format, upload and run settlement', async ({ page }) => { test.setTimeout(180000); feature('Settlement Operations'); tag('functional'); severity('critical'); description('Download XLSX from Transactions, strip headers/summary, upload formatted file to Settlement, click Start Settlement'); let downloadedFilePath = ''; let formattedFilePath = ''; let targetIso = ''; // Step 1: Download XLSX from Transactions await test.step('Navigate to Transactions', async () => { await navigateToTransactions(page); await page.waitForTimeout(5000); // Wait for table to load await page.locator('table tbody tr td').first().waitFor({ state: 'visible', timeout: 30000 }).catch(() => {}); await page.waitForTimeout(2000); const rows = await page.locator('table tbody tr').filter({ has: page.locator('td') }).count(); console.log(`Transactions rows: ${rows}`); expect(rows).toBeGreaterThan(0); }); await test.step('Determine target date from the latest transaction', async () => { // Anchor on the newest row actually in the table rather than a fixed // date - this dev dataset's "today" moves on, so a hardcoded date // eventually falls outside the calendar's visible range entirely and // silently selects nothing, which is why the download never fired. const table = page.locator('table').filter({ has: page.locator('th:has-text("RRN")') }).first(); await table.locator('tbody tr:not([aria-hidden="true"])').first().waitFor({ state: 'visible', timeout: 15000 }); const headers = await table.locator('thead th').allTextContents(); const dateIdx = headers.findIndex((h) => h.trim().toLowerCase() === 'init date'); expect(dateIdx, 'An init date column should exist').toBeGreaterThanOrEqual(0); const firstRowCells = await table.locator('tbody tr:not([aria-hidden="true"])').first().locator('td').allTextContents(); const latestDate = toDate(firstRowCells[dateIdx] || ''); expect(latestDate, 'The newest transaction should have a parseable date').not.toBeNull(); targetIso = toISO(latestDate!); console.log(`Target date from latest transaction (${firstRowCells[dateIdx]}): ${targetIso}`); }); await test.step('Open download report modal', async () => { const downloadBtn = page.locator('button:has-text("Download report")').first(); await downloadBtn.waitFor({ state: 'visible', timeout: 10000 }); await downloadBtn.click(); await page.waitForTimeout(2000); console.log('✓ Download modal opened'); }); await test.step('Select date range: latest transaction date', async () => { const dateField = page.locator('input[date-range="start"]').first(); await expect(dateField, 'Date range start input should be visible').toBeVisible({ timeout: 10000 }); await dateField.click(); await page.waitForTimeout(1500); // Click the target date twice — start and end on the same day await selectCalendarDate(page, targetIso, '.ant-picker-header-prev-btn'); await page.waitForTimeout(500); await selectCalendarDate(page, targetIso, '.ant-picker-header-prev-btn'); await page.waitForTimeout(1000); console.log(`✓ Date: ${targetIso}`); }); await test.step('Select XSLX and download', async () => { const xlsxRadio = page.locator('.ant-radio-button-wrapper', { hasText: 'XSLX' }).first(); const xlsxVis = await xlsxRadio.isVisible().catch(() => false); if (xlsxVis) { await xlsxRadio.click(); await page.waitForTimeout(2000); console.log('✓ XSLX selected'); } else { await page.locator('span:text-is("XSLX")').first().click(); await page.waitForTimeout(2000); } await page.waitForTimeout(3000); // Find the download button let dlBtn = page.locator('button:has-text("Download")').last(); let btnVisible = await dlBtn.isVisible({ timeout: 5000 }).catch(() => false); if (!btnVisible) { dlBtn = page.locator('button[type="button"]:has-text("Download")').first(); btnVisible = await dlBtn.isVisible({ timeout: 5000 }).catch(() => false); } console.log('Download button visible:', btnVisible); if (!btnVisible) { console.log('⚠ Download button not found. Skipping settlement test.'); test.skip(); return; } // Set up download listener - use same pattern as transactions test const downloadPromise = page.waitForEvent('download', { timeout: 30000 }).catch(() => null); // Scroll button into view await dlBtn.scrollIntoViewIfNeeded(); await page.waitForTimeout(500); // Click the download button console.log('Clicking download button...'); await dlBtn.click(); // Wait for download const download = await downloadPromise; if (download) { downloadedFilePath = (await download.path()) || ''; console.log('✓ Downloaded:', download.suggestedFilename()); } else { // Try to find the file in common download locations console.log('⚠ Download event did not fire, checking for file...'); const possiblePaths = [ '/tmp/transaction_report.xlsx', '/home/runner/Downloads/transaction_report.xlsx', process.env.HOME + '/Downloads/transaction_report.xlsx' ]; for (const possiblePath of possiblePaths) { if (fs.existsSync(possiblePath)) { downloadedFilePath = possiblePath; console.log('✓ Found downloaded file at:', downloadedFilePath); break; } } if (!downloadedFilePath) { console.log('⚠ Download failed: file not found and download event did not fire. Skipping settlement test.'); test.skip(); return; } } }); // Step 2: Parse XLSX, strip headers/summary, keep only APPROVED/DECLINED await test.step('Format downloaded file — keep only Approved/Declined transactions', async () => { expect(downloadedFilePath).toBeTruthy(); const fileBuffer = fs.readFileSync(downloadedFilePath); const workbook = XLSX.read(fileBuffer, { type: 'buffer' }); const sheetName = workbook.SheetNames[0]; const sheet = workbook.Sheets[sheetName]; const allRows: any[][] = XLSX.utils.sheet_to_json(sheet, { header: 1 }); console.log(`Original XLSX rows: ${allRows.length}`); for (let i = 0; i < Math.min(allRows.length, 10); i++) { console.log(`Row ${i}: ${JSON.stringify(allRows[i])?.substring(0, 150)}`); } // Find the header row (starts with "id") let headerIdx = -1; for (let i = 0; i < allRows.length; i++) { const firstCell = String(allRows[i]?.[0] || '').toLowerCase().trim(); if (firstCell === 'id') { headerIdx = i; break; } } console.log(`Header row index: ${headerIdx}`); expect(headerIdx).toBeGreaterThanOrEqual(0); const headerRow = allRows[headerIdx]; // Find the status column index let statusColIdx = -1; for (let c = 0; c < headerRow.length; c++) { if (String(headerRow[c]).toLowerCase().trim() === 'status') { statusColIdx = c; break; } } console.log(`Status column index: ${statusColIdx}`); // Find where SUMMARY starts let summaryIdx = allRows.length; for (let i = headerIdx + 1; i < allRows.length; i++) { const firstCell = String(allRows[i]?.[0] || '').toLowerCase().trim(); if (firstCell === '' || firstCell.includes('summary') || firstCell.includes('total')) { summaryIdx = i; break; } } // Get data rows and filter by Approved/Declined only. The raw export // values are "APPROVE"/"DECLINE" (not "APPROVED"/"DECLINED" like the UI // labels), so match by prefix rather than exact equality. const dataRows = allRows.slice(headerIdx + 1, summaryIdx); const filteredRows = dataRows.filter(row => { const status = String(row[statusColIdx] || '').toUpperCase().trim(); return status.startsWith('APPROVE') || status.startsWith('DECLINE'); }); console.log(`Data rows: ${dataRows.length}, After filter (Approved/Declined only): ${filteredRows.length}`); for (const row of filteredRows) { console.log(` Status: ${row[statusColIdx]}, ID: ${row[0]}`); } test.skip(filteredRows.length === 0, 'No Approved/Declined transactions available in this environment to settle'); // Build clean file: header + filtered data rows const cleanRows = [headerRow, ...filteredRows]; const newSheet = XLSX.utils.aoa_to_sheet(cleanRows); const newWorkbook = XLSX.utils.book_new(); XLSX.utils.book_append_sheet(newWorkbook, newSheet, 'Transactions'); formattedFilePath = path.join(path.dirname(downloadedFilePath), 'formatted_transactions.xlsx'); const outBuffer = XLSX.write(newWorkbook, { type: 'buffer', bookType: 'xlsx' }); fs.writeFileSync(formattedFilePath, outBuffer); console.log(`✓ Formatted file saved with ${filteredRows.length} transactions`); expect(fs.existsSync(formattedFilePath)).toBeTruthy(); }); // Step 3: Go to Settlement, select date, upload, start await test.step('Navigate to Settlement', async () => { await navigateToSettlement(page); await page.waitForTimeout(5000); // Wait for the page to fully load await page.locator('input[date-range="start"]').first().waitFor({ state: 'visible', timeout: 20000 }).catch(() => {}); await page.waitForTimeout(2000); console.log('✓ On Settlement page'); }); await test.step('Select date range on Settlement: latest transaction date', async () => { const dateField = page.locator('input[date-range="start"]').first(); await expect(dateField, 'Settlement date field should be visible').toBeVisible({ timeout: 10000 }); await dateField.click(); await page.waitForTimeout(2000); // Same date used for the Transactions download above, so the uploaded // file's transactions correspond to the settlement date being run. await selectCalendarDate(page, targetIso, '.ant-picker-prev-btn'); await page.waitForTimeout(500); await selectCalendarDate(page, targetIso, '.ant-picker-prev-btn'); await page.waitForTimeout(1000); console.log(`✓ Settlement date: ${targetIso}`); }); await test.step('Upload formatted file', async () => { expect(formattedFilePath).toBeTruthy(); const fileInput = page.locator('input[name="file"][type="file"]').first(); await fileInput.setInputFiles(formattedFilePath); await page.waitForTimeout(5000); console.log('✓ File uploaded'); const noFileMsg = await page.locator('text=/No file uploaded/i').isVisible().catch(() => false); console.log('No file message visible:', noFileMsg); expect(noFileMsg).toBeFalsy(); }); await test.step('Click Start Settlement', async () => { const startBtn = page.locator('button:has-text("Start Settlement")').first(); await startBtn.waitFor({ state: 'visible', timeout: 10000 }); await startBtn.click(); await page.waitForTimeout(5000); console.log('✓ Start Settlement clicked'); }); await test.step('Verify settlement results popup', async () => { await page.waitForTimeout(5000); // Check for a modal/popup with settlement results const modalText = await page.locator('.ant-modal, [role="dialog"]').first().textContent({ timeout: 10000 }).catch(() => ''); console.log('Settlement popup content:', modalText?.substring(0, 500)); // Check page body too const bodyText = await page.locator('body').textContent().catch(() => ''); const fullText = (modalText || '') + (bodyText || ''); const hasMatched = fullText.toLowerCase().includes('matched'); const hasMismatched = fullText.toLowerCase().includes('mismatched') || fullText.toLowerCase().includes('mismatch'); const hasNotMatched = fullText.toLowerCase().includes('not matched'); console.log('Has "matched":', hasMatched); console.log('Has "mismatched":', hasMismatched); console.log('Has "not matched":', hasNotMatched); // Log specific counts if visible const matchedCount = fullText.match(/(\d+)\s*matched/i); const mismatchedCount = fullText.match(/(\d+)\s*(?:mis|not\s*)matched/i); if (matchedCount) console.log('Matched count:', matchedCount[1]); if (mismatchedCount) console.log('Mismatched count:', mismatchedCount[1]); expect(hasMatched || hasMismatched || hasNotMatched).toBeTruthy(); }); console.log('✓ Settlement test completed'); }); });