Skip to content

Commit fd67166

Browse files
committed
Add accounting page for bank balance sheets
- Add /accounting route with Admin/Compliance guard - Add bank balance sheet T-account display (Soll/Haben) - Auto-select first bank on page load - Show opening balance, income, expenses, closing balance - Validate against defined closing balance (green/red indicator) - Add URL params for year and bank preselection - Add Playwright E2E tests for balance sheet functionality
1 parent c694860 commit fd67166

7 files changed

Lines changed: 680 additions & 0 deletions

File tree

e2e/accounting-balances.spec.ts

Lines changed: 308 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,308 @@
1+
import { test, expect, Page } from '@playwright/test';
2+
3+
/**
4+
* E2E Tests: Accounting Page - T-Account Balance Sheet
5+
*
6+
* Tests use the REAL API - no mocking allowed!
7+
*
8+
* Visual documentation of the accounting balance flow:
9+
* 1. Open accounting page
10+
* 2. Select year 2025
11+
* 3. Select bank "Maerki Baumann (CHF)"
12+
* 4. Verify T-account display with:
13+
* - Anfangsbestand (Opening Balance)
14+
* - Alle Einnahmen (Total Income)
15+
* - Alle Ausgaben (Total Expenses)
16+
* - Total row
17+
* - Saldo (Closing Balance)
18+
* - Validation message
19+
*
20+
* Each step produces a baseline screenshot for visual regression testing.
21+
*
22+
* Required env vars: ADMIN_ADDRESS, ADMIN_SIGNATURE
23+
*/
24+
25+
const ADMIN_ADDRESS = process.env.ADMIN_ADDRESS || '';
26+
const ADMIN_SIGNATURE = process.env.ADMIN_SIGNATURE || '';
27+
28+
// Bank IBANs for selection
29+
const MAERKI_CHF_IBAN = 'CH3408573177975200001';
30+
const MAERKI_EUR_IBAN = 'CH6808573177975201814';
31+
const RAIFFEISEN_CHF_IBAN = 'CH4880808002186504370';
32+
33+
async function removeErrorOverlay(page: Page): Promise<void> {
34+
await page.evaluate(() => {
35+
const overlay = document.getElementById('webpack-dev-server-client-overlay');
36+
if (overlay) overlay.remove();
37+
});
38+
}
39+
40+
async function waitForAppLoaded(page: Page): Promise<void> {
41+
await page.waitForTimeout(3000);
42+
}
43+
44+
async function waitForBalanceSheet(page: Page): Promise<void> {
45+
await page.waitForSelector('[data-testid="balance-sheet"]', { state: 'visible', timeout: 10000 });
46+
}
47+
48+
test.describe('Accounting Page - T-Account Balance Sheet (Real API)', () => {
49+
test.beforeAll(() => {
50+
if (!ADMIN_ADDRESS || !ADMIN_SIGNATURE) {
51+
throw new Error('ADMIN_ADDRESS and ADMIN_SIGNATURE must be set in .env');
52+
}
53+
});
54+
55+
test.describe('Step-by-Step Flow: Maerki Baumann CHF 2025', () => {
56+
/**
57+
* Step 1: Open the accounting page
58+
* - Page loads with admin authentication
59+
* - Shows title "DFX Accounting Report"
60+
* - First bank is auto-selected
61+
* - Balance sheet loads automatically
62+
*/
63+
test('Step 1: Open accounting page - auto-loads first bank', async ({ page }) => {
64+
await page.goto(`/accounting?address=${ADMIN_ADDRESS}&signature=${ADMIN_SIGNATURE}`);
65+
await waitForAppLoaded(page);
66+
await removeErrorOverlay(page);
67+
68+
// Verify page title is visible
69+
await expect(page.locator('h1')).toContainText('DFX Accounting Report');
70+
71+
// Verify dropdowns are visible
72+
const yearSelect = page.locator('select').first();
73+
const bankSelect = page.locator('select').nth(1);
74+
await expect(yearSelect).toBeVisible();
75+
await expect(bankSelect).toBeVisible();
76+
77+
// Wait for balance sheet to load (first bank is auto-selected)
78+
await waitForBalanceSheet(page);
79+
80+
// Balance sheet should be visible (first bank auto-selected)
81+
const balanceSheet = page.locator('[data-testid="balance-sheet"]');
82+
await expect(balanceSheet).toBeVisible();
83+
84+
// Capture baseline screenshot
85+
await expect(page).toHaveScreenshot('01-accounting-page-opened.png', {
86+
fullPage: true,
87+
animations: 'disabled',
88+
});
89+
});
90+
91+
/**
92+
* Step 2: Select Maerki Baumann CHF and year 2025
93+
* - T-Account table appears with real data from API
94+
*/
95+
test('Step 2: Select Maerki Baumann CHF 2025 - shows T-account', async ({ page }) => {
96+
await page.goto(`/accounting?address=${ADMIN_ADDRESS}&signature=${ADMIN_SIGNATURE}`);
97+
await waitForAppLoaded(page);
98+
await removeErrorOverlay(page);
99+
100+
// Select year 2025
101+
const yearSelect = page.locator('select').first();
102+
await yearSelect.selectOption('2025');
103+
await page.waitForTimeout(300);
104+
105+
// Select Maerki Baumann (CHF) by IBAN
106+
const bankSelect = page.locator('select').nth(1);
107+
await bankSelect.selectOption(MAERKI_CHF_IBAN);
108+
109+
// Wait for balance sheet to load from real API
110+
await waitForBalanceSheet(page);
111+
112+
// Verify bank is selected
113+
await expect(bankSelect).toHaveValue(MAERKI_CHF_IBAN);
114+
115+
// Verify T-account is visible
116+
const balanceSheet = page.locator('[data-testid="balance-sheet"]');
117+
await expect(balanceSheet).toBeVisible();
118+
119+
// Verify IBAN is displayed
120+
await expect(balanceSheet).toContainText(MAERKI_CHF_IBAN);
121+
122+
// Verify opening balance is displayed (value from yearlyBalances in DB)
123+
const openingBalance = page.locator('[data-testid="opening-balance"]');
124+
await expect(openingBalance).toBeVisible();
125+
126+
// Verify total income is displayed
127+
const totalIncome = page.locator('[data-testid="total-income"]');
128+
await expect(totalIncome).toBeVisible();
129+
130+
// Verify total expenses is displayed
131+
const totalExpenses = page.locator('[data-testid="total-expenses"]');
132+
await expect(totalExpenses).toBeVisible();
133+
134+
// Verify closing balance (Saldo) is displayed
135+
const closingBalance = page.locator('[data-testid="closing-balance"]');
136+
await expect(closingBalance).toBeVisible();
137+
138+
// Capture baseline screenshot with T-account data
139+
await expect(page).toHaveScreenshot('02-maerki-chf-t-account-displayed.png', {
140+
fullPage: true,
141+
animations: 'disabled',
142+
});
143+
});
144+
});
145+
146+
test.describe('Additional Balance Scenarios', () => {
147+
/**
148+
* Maerki Baumann EUR 2025
149+
*/
150+
test('Maerki Baumann EUR 2025 T-account', async ({ page }) => {
151+
await page.goto(`/accounting?address=${ADMIN_ADDRESS}&signature=${ADMIN_SIGNATURE}`);
152+
await waitForAppLoaded(page);
153+
await removeErrorOverlay(page);
154+
155+
const yearSelect = page.locator('select').first();
156+
await yearSelect.selectOption('2025');
157+
158+
const bankSelect = page.locator('select').nth(1);
159+
await bankSelect.selectOption(MAERKI_EUR_IBAN);
160+
161+
await waitForBalanceSheet(page);
162+
163+
const balanceSheet = page.locator('[data-testid="balance-sheet"]');
164+
await expect(balanceSheet).toBeVisible();
165+
166+
// Verify IBAN is displayed
167+
await expect(balanceSheet).toContainText(MAERKI_EUR_IBAN);
168+
169+
// Verify opening balance is displayed
170+
const openingBalance = page.locator('[data-testid="opening-balance"]');
171+
await expect(openingBalance).toBeVisible();
172+
173+
// Verify closing balance is displayed
174+
const closingBalance = page.locator('[data-testid="closing-balance"]');
175+
await expect(closingBalance).toBeVisible();
176+
177+
await expect(page).toHaveScreenshot('03-maerki-eur-t-account-displayed.png', {
178+
fullPage: true,
179+
animations: 'disabled',
180+
});
181+
});
182+
183+
/**
184+
* Raiffeisen CHF 2025
185+
*/
186+
test('Raiffeisen CHF 2025 T-account', async ({ page }) => {
187+
await page.goto(`/accounting?address=${ADMIN_ADDRESS}&signature=${ADMIN_SIGNATURE}`);
188+
await waitForAppLoaded(page);
189+
await removeErrorOverlay(page);
190+
191+
const yearSelect = page.locator('select').first();
192+
await yearSelect.selectOption('2025');
193+
194+
const bankSelect = page.locator('select').nth(1);
195+
await bankSelect.selectOption(RAIFFEISEN_CHF_IBAN);
196+
197+
await waitForBalanceSheet(page);
198+
199+
const balanceSheet = page.locator('[data-testid="balance-sheet"]');
200+
await expect(balanceSheet).toBeVisible();
201+
202+
// Verify IBAN is displayed
203+
await expect(balanceSheet).toContainText(RAIFFEISEN_CHF_IBAN);
204+
205+
// Verify opening balance is displayed
206+
const openingBalance = page.locator('[data-testid="opening-balance"]');
207+
await expect(openingBalance).toBeVisible();
208+
209+
// Verify closing balance is displayed
210+
const closingBalance = page.locator('[data-testid="closing-balance"]');
211+
await expect(closingBalance).toBeVisible();
212+
213+
await expect(page).toHaveScreenshot('04-raiffeisen-chf-t-account-displayed.png', {
214+
fullPage: true,
215+
animations: 'disabled',
216+
});
217+
});
218+
219+
/**
220+
* Year 2024 - test with different year
221+
*/
222+
test('Year 2024 - balance sheet loads', async ({ page }) => {
223+
await page.goto(`/accounting?address=${ADMIN_ADDRESS}&signature=${ADMIN_SIGNATURE}`);
224+
await waitForAppLoaded(page);
225+
await removeErrorOverlay(page);
226+
227+
// Select year 2024
228+
const yearSelect = page.locator('select').first();
229+
await yearSelect.selectOption('2024');
230+
231+
// Select Raiffeisen
232+
const bankSelect = page.locator('select').nth(1);
233+
await bankSelect.selectOption(RAIFFEISEN_CHF_IBAN);
234+
235+
await waitForBalanceSheet(page);
236+
237+
// Balance sheet should be visible
238+
const balanceSheet = page.locator('[data-testid="balance-sheet"]');
239+
await expect(balanceSheet).toBeVisible();
240+
241+
// Verify opening and closing balance are displayed
242+
const openingBalance = page.locator('[data-testid="opening-balance"]');
243+
await expect(openingBalance).toBeVisible();
244+
245+
const closingBalance = page.locator('[data-testid="closing-balance"]');
246+
await expect(closingBalance).toBeVisible();
247+
248+
await expect(page).toHaveScreenshot('05-year-2024-balance-sheet.png', {
249+
fullPage: true,
250+
animations: 'disabled',
251+
});
252+
});
253+
});
254+
255+
test.describe('Direct URL with Preselected Parameters', () => {
256+
/**
257+
* Test direct URL access with year and bank preselected via URL params
258+
* URL format: /accounting?year=2025&bank=CH3408573177975200001&address=...&signature=...
259+
*
260+
* Expected behavior:
261+
* - Year dropdown shows 2025
262+
* - Bank dropdown shows "Maerki Baumann (CHF)"
263+
* - T-account balance sheet loads automatically from real API
264+
*/
265+
test('Direct link with year=2025 and bank=Maerki Baumann CHF', async ({ page }) => {
266+
// Navigate using direct URL with all parameters preselected
267+
const directUrl = `/accounting?year=2025&bank=${MAERKI_CHF_IBAN}&address=${ADMIN_ADDRESS}&signature=${ADMIN_SIGNATURE}`;
268+
await page.goto(directUrl);
269+
await waitForAppLoaded(page);
270+
await removeErrorOverlay(page);
271+
272+
// Verify page title
273+
await expect(page.locator('h1')).toContainText('DFX Accounting Report');
274+
275+
// Verify year is preselected to 2025
276+
const yearSelect = page.locator('select').first();
277+
await expect(yearSelect).toHaveValue('2025');
278+
279+
// Verify bank is preselected to Maerki Baumann CHF
280+
const bankSelect = page.locator('select').nth(1);
281+
await expect(bankSelect).toHaveValue(MAERKI_CHF_IBAN);
282+
283+
// Wait for balance sheet to load from real API
284+
await waitForBalanceSheet(page);
285+
286+
// Verify T-account balance sheet is visible (auto-loaded)
287+
const balanceSheet = page.locator('[data-testid="balance-sheet"]');
288+
await expect(balanceSheet).toBeVisible();
289+
290+
// Verify IBAN is displayed in balance sheet
291+
await expect(balanceSheet).toContainText(MAERKI_CHF_IBAN);
292+
293+
// Verify opening balance is displayed
294+
const openingBalance = page.locator('[data-testid="opening-balance"]');
295+
await expect(openingBalance).toBeVisible();
296+
297+
// Verify closing balance (Saldo) is displayed
298+
const closingBalance = page.locator('[data-testid="closing-balance"]');
299+
await expect(closingBalance).toBeVisible();
300+
301+
// Capture screenshot of direct link result
302+
await expect(page).toHaveScreenshot('06-direct-link-maerki-chf-2025.png', {
303+
fullPage: true,
304+
animations: 'disabled',
305+
});
306+
});
307+
});
308+
});
54 KB
Loading

playwright.config.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
11
import { defineConfig, devices } from '@playwright/test';
2+
import dotenv from 'dotenv';
3+
4+
// Load .env file for test configuration
5+
dotenv.config();
26

37
export default defineConfig({
48
testDir: './e2e',

src/App.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ const RealunitScreen = lazy(() => import('./screens/realunit.screen'));
5959
const RealunitUserScreen = lazy(() => import('./screens/realunit-user.screen'));
6060
const PersonalIbanScreen = lazy(() => import('./screens/personal-iban.screen'));
6161
const BuyCryptoUpdateScreen = lazy(() => import('./screens/buy-crypto-update.screen'));
62+
const AccountingScreen = lazy(() => import('./screens/accounting.screen'));
6263

6364
setupLanguages();
6465

@@ -320,6 +321,10 @@ export const Routes = [
320321
path: 'compliance',
321322
element: withSuspense(<ComplianceScreen />),
322323
},
324+
{
325+
path: 'accounting',
326+
element: withSuspense(<AccountingScreen />),
327+
},
323328
{
324329
path: 'compliance/user/:id',
325330
element: withSuspense(<ComplianceUserScreen />),

src/components/navigation.tsx

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -206,6 +206,15 @@ function NavigationMenu({ setIsNavigationOpen, small = false }: NavigationMenuCo
206206
onClose={() => setIsNavigationOpen(false)}
207207
/>
208208
)}
209+
{session?.role && [UserRole.ADMIN, UserRole.COMPLIANCE].includes(session.role) && (
210+
<NavigationLink
211+
icon={IconVariant.TRANSACTIONS}
212+
label={translate('screens/accounting', 'Accounting')}
213+
url="/accounting"
214+
target="_self"
215+
onClose={() => setIsNavigationOpen(false)}
216+
/>
217+
)}
209218
{session?.role && [UserRole.ADMIN].includes(session.role) && (
210219
<NavigationLink
211220
icon={IconVariant.WALLET}

0 commit comments

Comments
 (0)