import { test, expect } from '@playwright/test'; import { login } from './helpers/auth'; import { epic, feature, severity, description, tag } from './helpers/allure'; import { navigateToTransactions, navigateToDiscounts, navigateToSettings } from './helpers/navigation'; test.describe('Discounts & Fees Tests', () => { test.setTimeout(300000); test('should calculate discount and verify against transactions page', async ({ page }) => { feature('Discounts Operations'); tag('functional'); severity('critical'); description('Get latest transaction date, calculate MDR+VAT+FEE from transactions, then verify matches Discounts page totals'); epic('Discounts & Fees'); let latestDate = ''; let txnTotalMDR = 0, txnTotalVAT = 0, txnTotalFee = 0, txnTotalDiscount = 0; // Step 1: Go to Transactions page await test.step('Step 1: Go to Transactions page', async () => { await login(page); await navigateToTransactions(page); await page.waitForTimeout(2000); console.log('✓ On Transactions page'); }); // Step 2: Find latest date with transactions await test.step('Step 2: Find latest date with transactions', async () => { await page.waitForTimeout(1000); // Scroll the table horizontally to reveal the init date column const tableContent = page.locator('.ant-table-content').first(); await tableContent.evaluate((element) => { // Scroll to the right to reveal the init date column element.scrollLeft = element.scrollWidth; }); await page.waitForTimeout(500); console.log('✓ Scrolled table horizontally to reveal init date column'); // Get the first real data row (skip the hidden ant-table-measure-row) const firstRow = page.locator('tr.ant-table-row').first(); const rowText = await firstRow.textContent(); console.log('Full first row text after scroll:', rowText?.substring(0, 200)); // Extract date from the row (format: "Mon DD, YYYY" like "Jun 19, 2026") const dateMatch = rowText?.match(/(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)\s+\d{1,2},\s*\d{4}/); if (dateMatch) { latestDate = dateMatch[0]; console.log('\n========== DATE EXTRACTED =========='); console.log('✓ Latest transaction date:', latestDate); console.log('====================================\n'); } else { console.log('⚠ Could not extract date from first row'); throw new Error('Failed to extract date from transaction row'); } }); // Step 3: Count how many transaction rows fall within the latest date await test.step('Step 3: Count transactions within the latest date', async () => { await page.waitForTimeout(500); const monthRegex = /(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)\s+\d{1,2},\s*\d{4}/; let totalMatching = 0; let currentPage = 1; let keepPaging = true; while (keepPaging) { // Wait for the table body to be present before reading await page.locator('tr.ant-table-row').first().waitFor({ state: 'visible', timeout: 10000 }); const rows = page.locator('tr.ant-table-row'); const rowCount = await rows.count(); // Capture the first row key on this page (used to detect page change later) const firstKey = await rows.first().getAttribute('data-row-key'); const lastKey = await rows.nth(rowCount - 1).getAttribute('data-row-key'); // Read the actual page footer ("Page X of Y") for verification const footer = await page.locator('text=/Page\\s+\\d+\\s+of\\s+\\d+/').first().textContent().catch(() => null); let pageMatching = 0; for (let i = 0; i < rowCount; i++) { const rowText = await rows.nth(i).textContent(); const match = rowText?.match(monthRegex); const rowDate = match ? match[0] : ''; if (rowDate === latestDate) { pageMatching++; } } totalMatching += pageMatching; console.log(`[${footer?.trim() || 'page ' + currentPage}] firstID=${firstKey} lastID=${lastKey} -> ${pageMatching}/${rowCount} on ${latestDate}`); // If not every row on this page matched, we've reached older dates - stop if (pageMatching < rowCount) { keepPaging = false; break; } // All rows matched - go to next page const nextPage = currentPage + 1; const nextPageBtn = page.locator(`button:has-text("${nextPage}")`).first(); const hasNextPage = await nextPageBtn.isVisible({ timeout: 2000 }).catch(() => false); if (!hasNextPage) { keepPaging = false; break; } await nextPageBtn.click(); // Wait until the first row's key actually changes (confirms the page re-rendered) await page.waitForFunction( (prevKey) => { const row = document.querySelector('tr.ant-table-row'); return row && row.getAttribute('data-row-key') !== prevKey; }, firstKey, { timeout: 10000 } ).catch(() => { console.log('⚠ Page did not change after clicking next - stopping'); }); currentPage = nextPage; } console.log('\n========== ROW COUNT =========='); console.log(`✓ Total transactions on ${latestDate}: ${totalMatching}`); console.log('===============================\n'); }); // Step 4: Enable MDR, VAT, Fee columns via Manage columns modal await test.step('Step 4: Enable MDR, VAT, Fee columns', async () => { // Open the Manage columns modal const manageColumnsBtn = page.locator('button:has-text("Manage columns"), button:has-text("columns")').first(); await manageColumnsBtn.click(); await page.waitForTimeout(1000); console.log('✓ Opened Manage columns modal'); // Scope everything to the modal dialog so we don't hit the page's own search/checkboxes const modal = page.locator('.ant-modal-content').first(); const searchInput = modal.locator('input[placeholder="Search"]').first(); // Helper: search for a column and enable its checkbox if not already checked const enableColumn = async (term: string) => { await searchInput.fill(''); await page.waitForTimeout(300); await searchInput.fill(term); await page.waitForTimeout(700); // After filtering, the matching column's checkbox should be the visible one const checkbox = modal.locator('input.ant-checkbox-input').first(); const exists = await checkbox.isVisible({ timeout: 3000 }).catch(() => false); if (!exists) { console.log(`⚠ No checkbox found for "${term}"`); return; } const isChecked = await checkbox.isChecked().catch(() => false); if (isChecked) { console.log(`• "${term}" column already enabled`); } else { await checkbox.click(); await page.waitForTimeout(400); console.log(`✓ Enabled "${term}" column`); } }; await enableColumn('mdr'); await enableColumn('vat'); await enableColumn('fee'); // Clear the search before closing await searchInput.fill(''); await page.waitForTimeout(300); // Click Done to apply const doneBtn = modal.locator('button:has-text("Done")').first(); await doneBtn.click(); await page.waitForTimeout(1000); console.log('✓ Applied column selection (clicked Done)'); }); // Step 5: Sum MDR + VAT + Fee across all transactions on the latest date await test.step('Step 5: Sum MDR + VAT + Fee for the latest date', async () => { const monthRegex = /(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)\s+\d{1,2},\s*\d{4}/; // Go back to page 1 (confirm the table actually re-renders) const beforeKey = await page.locator('tr.ant-table-row').first().getAttribute('data-row-key'); const firstPageBtn = page.getByRole('button', { name: '1', exact: true }).first(); if (await firstPageBtn.isVisible({ timeout: 3000 }).catch(() => false)) { await firstPageBtn.scrollIntoViewIfNeeded().catch(() => {}); await firstPageBtn.click(); await page.waitForFunction( (prevKey) => { const r = document.querySelector('tr.ant-table-row'); return r && r.getAttribute('data-row-key') !== prevKey; }, beforeKey, { timeout: 10000 } ).catch(() => console.log('⚠ Did not return to page 1 cleanly')); await page.waitForTimeout(500); } console.log(`Back on page 1, first row ID: ${await page.locator('tr.ant-table-row').first().getAttribute('data-row-key')}`); // Determine column indexes for MDR, VAT, Fee from the table header const headerCells = page.locator('thead th'); const headerCount = await headerCells.count(); let mdrIdx = -1, vatIdx = -1, feeIdx = -1; for (let i = 0; i < headerCount; i++) { const t = (await headerCells.nth(i).textContent())?.trim(); if (t === 'MDR') mdrIdx = i; else if (t === 'VAT') vatIdx = i; else if (t === 'Fee') feeIdx = i; } console.log(`Column indexes -> MDR:${mdrIdx} VAT:${vatIdx} Fee:${feeIdx}`); if (mdrIdx < 0 || vatIdx < 0 || feeIdx < 0) { throw new Error('Could not locate MDR/VAT/Fee columns in header'); } // Read a numeric value from a cell: the value sits in the last
, // separate from the currency-icon SVG article. const parseCell = async (cell: ReturnType): Promise => { const valArticle = cell.locator('article').last(); const txt = (await valArticle.textContent().catch(() => '')) || ''; const num = parseFloat(txt.replace(/[^0-9.\-]/g, '')); return isNaN(num) ? 0 : num; }; let totalMDR = 0, totalVAT = 0, totalFee = 0, totalRows = 0; let currentPage = 1; let keepPaging = true; while (keepPaging) { await page.locator('tr.ant-table-row').first().waitFor({ state: 'visible', timeout: 10000 }); const rows = page.locator('tr.ant-table-row'); const rowCount = await rows.count(); const firstKey = await rows.first().getAttribute('data-row-key'); let pageMatching = 0; for (let i = 0; i < rowCount; i++) { const row = rows.nth(i); const rowText = await row.textContent(); const m = rowText?.match(monthRegex); const rowDate = m ? m[0] : ''; if (rowDate !== latestDate) continue; const cells = row.locator('td'); const mdr = await parseCell(cells.nth(mdrIdx)); const vat = await parseCell(cells.nth(vatIdx)); const fee = await parseCell(cells.nth(feeIdx)); const rowDiscount = mdr + vat + fee; totalMDR += mdr; totalVAT += vat; totalFee += fee; totalRows++; pageMatching++; const id = await row.getAttribute('data-row-key'); console.log(` Row ${id}: MDR=${mdr.toFixed(2)} VAT=${vat.toFixed(2)} Fee=${fee.toFixed(2)} -> ${rowDiscount.toFixed(2)}`); } console.log(`[page ${currentPage}] matched ${pageMatching}/${rowCount} rows`); // If not every row matched, we've reached older dates - stop if (pageMatching < rowCount) { keepPaging = false; break; } // Go to next page const nextPage = currentPage + 1; const nextPageBtn = page.getByRole('button', { name: `${nextPage}`, exact: true }).first(); const hasNextPage = await nextPageBtn.isVisible({ timeout: 2000 }).catch(() => false); if (!hasNextPage) { keepPaging = false; break; } await nextPageBtn.click(); await page.waitForFunction( (prevKey) => { const row = document.querySelector('tr.ant-table-row'); return row && row.getAttribute('data-row-key') !== prevKey; }, firstKey, { timeout: 10000 } ).catch(() => { console.log('⚠ Page did not change after clicking next - stopping'); }); currentPage = nextPage; } const grandTotal = totalMDR + totalVAT + totalFee; txnTotalMDR = totalMDR; txnTotalVAT = totalVAT; txnTotalFee = totalFee; txnTotalDiscount = grandTotal; console.log('\n========== DISCOUNT TOTALS (Transactions page) =========='); console.log(`Date: ${latestDate}`); console.log(`Rows summed: ${totalRows}`); console.log(`Total MDR: ${totalMDR.toFixed(2)}`); console.log(`Total VAT: ${totalVAT.toFixed(2)}`); console.log(`Total Fee: ${totalFee.toFixed(2)}`); console.log(`Total Discount (MDR+VAT+Fee): ${grandTotal.toFixed(2)}`); console.log('=========================================================\n'); }); // Step 6: Go to Discounts page and filter by the transaction date await test.step('Step 6: Go to Discounts page and filter by date', async () => { await navigateToDiscounts(page); await page.waitForTimeout(2000); console.log('✓ On Discounts & Fees page'); // Convert latestDate ("Jun 19, 2026") to picker title format ("2026-06-19") 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', }; const parts = latestDate.match(/(\w+)\s+(\d{1,2}),\s*(\d{4})/); if (!parts) { throw new Error(`Could not parse latestDate: ${latestDate}`); } const titleDate = `${parts[3]}-${months[parts[1]]}-${parts[2].padStart(2, '0')}`; console.log(`Target date cell title: ${titleDate}`); // Open the date range picker by clicking the start date input const startInput = page.locator('input[placeholder="Start date"]').first(); await startInput.click(); await page.waitForTimeout(1000); console.log('✓ Opened date range picker'); // The calendar opens on the current month - page back until the // target month is in view (this dataset's dates can be well over a // year old, so the calendar needs more than a couple of clicks back). const dayCell = page.locator(`td[title="${titleDate}"]`).first(); const prevBtn = page.locator('.ant-picker-header-prev-btn').first(); let dayCellFound = false; for (let attempt = 0; attempt < 60; attempt++) { dayCellFound = await dayCell.isVisible().catch(() => false); if (dayCellFound) break; await prevBtn.click(); await page.waitForTimeout(300); } expect(dayCellFound, `Target date ${titleDate} should become available in the calendar`).toBe(true); // Click the target day cell for the start of the range await dayCell.click(); await page.waitForTimeout(500); console.log('✓ Selected start date'); // Click the same day cell again for the end of the range (single-day filter) const dayCellEnd = page.locator(`td[title="${titleDate}"]`).first(); await dayCellEnd.click(); await page.waitForTimeout(1500); console.log('✓ Selected end date (same day)'); // Verify the filter actually took: read back the start/end input values const startVal = await page.locator('input[date-range="start"], input[placeholder="Start date"]').first().inputValue().catch(() => ''); const endVal = await page.locator('input[date-range="end"], input[placeholder="End date"]').first().inputValue().catch(() => ''); console.log(`Start input value: "${startVal}"`); console.log(`End input value: "${endVal}"`); const pickerOpen = await page.locator('.ant-picker-dropdown:visible').count().catch(() => 0); console.log(`Picker dropdown still open: ${pickerOpen > 0 ? 'yes' : 'no'}`); if (startVal === titleDate && endVal === titleDate) { console.log(`✓ Date range correctly applied: ${titleDate} to ${titleDate}`); } else { console.log(`⚠ Date range may not have applied correctly (expected ${titleDate} for both)`); } console.log(`✓ Filtered Discounts page to ${latestDate}`); }); // Step 7: Read the Discounts-page total and cross-check against Transactions await test.step('Step 7: Cross-check total discount', async () => { await page.waitForTimeout(1500); const parseNum = (s: string | null | undefined): number => { const n = parseFloat((s || '').replace(/[^0-9.\-]/g, '')); return isNaN(n) ? 0 : n; }; // Top "Total Discount" card (large number) - this is the source of truth const totalDiscountCard = page.locator('article.text-4xl').first(); const cardText = await totalDiscountCard.textContent().catch(() => ''); const discountPageTotal = parseNum(cardText); // Scheme Discount Breakdown table (scoped via its heading) for internal detail const schemeTable = page.locator('xpath=//*[contains(text(),"Scheme Discount Breakdown")]/following::table[1]').first(); let breakdownMDR = 0, breakdownVAT = 0, breakdownFee = 0, breakdownRed = 0; if (await schemeTable.isVisible({ timeout: 3000 }).catch(() => false)) { // Map header columns to indexes const headers = schemeTable.locator('thead th'); const headerCount = await headers.count(); let mdrIdx = -1, vatIdx = -1, feeIdx = -1, dfIdx = -1; for (let i = 0; i < headerCount; i++) { const t = (await headers.nth(i).textContent())?.trim().toLowerCase(); if (t === 'mdr') mdrIdx = i; else if (t === 'vat') vatIdx = i; else if (t?.includes('processing') || t === 'fee') feeIdx = i; else if (t?.includes('discounts')) dfIdx = i; } // Sum across all card-scheme rows. Each amount cell renders a // currency-icon SVG (with an embedded