Damit ist der geplante Funktionsumfang beisammen. Phase 4 abgeschlossen - Konfigurierbare Cockpit-Kacheln; abgewählte werden nicht berechnet - Einnahmen/Ausgaben je Monat als Balkenpaare mit Saldolinie, aufgesetzt auf den Summenbericht statt auf eine eigene Abfrage - Meine Ausgaben als Ring auf der obersten Kategorieebene - Offene Vorgänge aus allen Modulen Phase 5 – Online-Banking - nemiah/php-fints als Fork auf Codeberg, psr/log direkt eingebaut: die Anwendung hängt an keinem fremden Update-Zyklus - PIN AES-256-GCM-verschlüsselt; Schlüssel aus APP_KEY oder aus data/schluessel/app_key, den der Entrypoint einmalig erzeugt - TAN-Dialog über mehrere Requests (FinTs::persist plus serialisierte Aktion in der Sitzung) – ein wartender Prozess wäre mit php-fpm der falsche Weg - Abgleich statt Direktbuchung: verbinden, übernehmen oder verwerfen - Regeln mit vier Vergleichsarten, Betragsgrenzen und Split-Schema Phase 6 – Wertpapiere - Depotbuchung und Geldbuchung als zwei Seiten eines Geschäfts; betrag ist der Bruttowert, Nebenkosten stehen daneben - Einstand nach gleitendem Durchschnitt, realisierte Gewinne, Erträge - Kursabruf über Leeway oder FMP, täglicher Lauf per Cron - QIF-Import für !Type:Invst Dabei behoben: Der Import buchte den Geldfluss jedes Wertpapiergeschäfts als Abgang – auch bei Verkäufen und Dividenden; und er erkannte das Verrechnungskonto nicht, wenn dem Konto im L-Feld eine Kategorie vorangeht (bei Verkäufen also immer). Phase 7 – Nebenmodule und Zahlungsverkehr - Buchungsvorlagen inklusive QIF-Import, Splits werden mitskaliert - Regelmäßige Buchungen mit Fälligkeitslauf, Finanzkalender, Erinnerungen - Adressen mit IBAN-Prüfung, Verträge mit Kündigungsfrist, Sparziele - Kredite mit gerechnetem Tilgungsplan - Überweisungen über FinTS; die Buchung entsteht erst nach Annahme durch die Bank. Die pain.001-Nachricht erzeugt SepaXml selbst Nebenbei: .dockerignore ergänzt (ohne sie brach der Build an data/mariadb ab), PlatzhalterController entfernt, zwei Inline-Handler beseitigt, die die Content-Security-Policy ohnehin blockierte. 888 PHP-Prüfungen und 45 E2E-Tests grün. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
164 lines
6 KiB
JavaScript
164 lines
6 KiB
JavaScript
// @ts-check
|
||
const { test, expect } = require('@playwright/test')
|
||
|
||
const BENUTZER = process.env.E2E_USER || 'e2e'
|
||
const PASSWORT = process.env.E2E_PASSWORD || 'e2e-testpasswort'
|
||
|
||
async function anmelden(page) {
|
||
await page.goto('/login')
|
||
|
||
if (page.url().includes('/einrichtung')) {
|
||
throw new Error(
|
||
'Instanz ist noch nicht eingerichtet. Zuerst einen Benutzer anlegen: ' +
|
||
'docker compose exec -u www-data php php scripts/e2e-benutzer.php'
|
||
)
|
||
}
|
||
|
||
await page.fill('#benutzername', BENUTZER)
|
||
await page.fill('#passwort', PASSWORT)
|
||
await page.click('button[type="submit"]')
|
||
await expect(page).toHaveURL(/\/$/)
|
||
}
|
||
|
||
/**
|
||
* Die Module aus den Phasen 4 bis 7: Cockpit-Kacheln, Online-Banking,
|
||
* Wertpapiere und die Nebenmodule.
|
||
*
|
||
* Geprüft wird, dass jeder Bereich erreichbar ist und seine Kernansicht
|
||
* aufbaut – nicht die Rechenlogik. Die steckt in der PHP-Suite, wo sie
|
||
* schneller und isoliert läuft.
|
||
*/
|
||
test.describe('Module der Phasen 4 bis 7', () => {
|
||
test.beforeEach(async ({ page }) => {
|
||
await anmelden(page)
|
||
})
|
||
|
||
test('Jeder Navigationspunkt führt in ein gebautes Modul', async ({ page }) => {
|
||
const punkte = [
|
||
['/konten', 'Kontenübersicht'],
|
||
['/banking', 'Bankverbindungen'],
|
||
['/zahlungsverkehr', 'Zahlungsverkehr'],
|
||
['/auswertungen', 'Auswertungen'],
|
||
['/planung', 'Planung'],
|
||
['/termine', 'Regelmäßige Buchungen'],
|
||
['/vertraege', 'Verträge'],
|
||
['/wertpapiere', 'Wertpapiere'],
|
||
['/verwaltung', 'Verwaltung'],
|
||
]
|
||
|
||
for (const [pfad, titel] of punkte) {
|
||
await page.goto(pfad)
|
||
await expect(page.locator('h1')).toContainText(titel)
|
||
// Kein Platzhalter mehr: Alle Bereiche haben ihr Modul.
|
||
await expect(page.locator('.phasenmarke')).toHaveCount(0)
|
||
}
|
||
})
|
||
|
||
test('Das Cockpit zeichnet seine Kacheln', async ({ page }) => {
|
||
await page.goto('/')
|
||
|
||
// Über die Überschriften, nicht über den Kacheltext: „Kontostände“ kommt
|
||
// auch im Hinweis der Vermögenskachel vor.
|
||
const ueberschriften = page.locator('.kachel h2')
|
||
for (const titel of [
|
||
'Vermögensentwicklung', 'Einnahmen und Ausgaben', 'Meine Ausgaben', 'Kontostände',
|
||
]) {
|
||
await expect(ueberschriften.filter({ hasText: titel })).toHaveCount(1)
|
||
}
|
||
})
|
||
|
||
test('Abgewählte Kacheln verschwinden und kommen zurück', async ({ page }) => {
|
||
await page.goto('/einstellungen?bereich=darstellung')
|
||
|
||
const kachel = page.locator('input[name="cockpit_kacheln[]"][value="ausgaben"]')
|
||
await kachel.uncheck()
|
||
await page.click('button:has-text("Speichern")')
|
||
|
||
await page.goto('/')
|
||
await expect(page.locator('.kachel h2', { hasText: 'Meine Ausgaben' })).toHaveCount(0)
|
||
await expect(page.locator('.kachelfuss')).toContainText('abgeschaltet')
|
||
|
||
await page.goto('/einstellungen?bereich=darstellung')
|
||
await page.locator('input[name="cockpit_kacheln[]"][value="ausgaben"]').check()
|
||
await page.click('button:has-text("Speichern")')
|
||
|
||
await page.goto('/')
|
||
await expect(page.locator('.kachel h2', { hasText: 'Meine Ausgaben' })).toHaveCount(1)
|
||
})
|
||
|
||
test('Eine Bankverbindung lässt sich anlegen und wieder löschen', async ({ page }) => {
|
||
await page.goto('/banking/neu')
|
||
|
||
await page.fill('#name', 'E2E-Testbank')
|
||
await page.fill('#blz', '12030000')
|
||
await page.fill('#fints_url', 'https://banking.example.org/fints')
|
||
await page.fill('#benutzerkennung', 'e2e-kunde')
|
||
await page.fill('#pin', 'geheim')
|
||
await page.click('button:has-text("Anlegen")')
|
||
|
||
await expect(page.locator('.meldung')).toContainText('angelegt')
|
||
await expect(page.locator('h1')).toContainText('E2E-Testbank')
|
||
|
||
// Die PIN wird nie wieder angezeigt.
|
||
await expect(page.locator('#pin')).toHaveValue('')
|
||
await expect(page.locator('#pin')).toHaveAttribute('placeholder', /unverändert/)
|
||
|
||
page.once('dialog', dialog => dialog.accept())
|
||
await page.click('button:has-text("Bankverbindung löschen")')
|
||
await expect(page.locator('.meldung')).toContainText('gelöscht')
|
||
})
|
||
|
||
test('Eine falsche IBAN wird beim Anlegen einer Adresse abgewiesen', async ({ page }) => {
|
||
await page.goto('/vertraege/adressen')
|
||
|
||
await page.fill('#name', 'E2E-Falschtest')
|
||
await page.fill('#iban', 'DE02120300000000202052')
|
||
await page.click('button:has-text("Adresse anlegen")')
|
||
|
||
await expect(page.locator('.meldung-fehler')).toContainText('IBAN')
|
||
})
|
||
|
||
test('Ein Kredit zeigt seinen gerechneten Tilgungsplan', async ({ page }) => {
|
||
await page.goto('/vertraege/kredite')
|
||
|
||
await page.fill('#name', 'E2E-Darlehen')
|
||
await page.fill('#darlehensbetrag', '100000')
|
||
await page.fill('#zinssatz', '3,0')
|
||
await page.fill('#rate', '500')
|
||
await page.click('button:has-text("Kredit anlegen")')
|
||
|
||
await expect(page.locator('h3', { hasText: 'Tilgungsplan' })).toContainText('E2E-Darlehen')
|
||
await expect(page.locator('.kennzahltitel', { hasText: 'Zinsen gesamt' })).toHaveCount(1)
|
||
|
||
// Erste Rate: 250 Zinsen, 250 Tilgung. Die erste Tabelle ist die
|
||
// Kreditliste, der Plan steht in der zweiten.
|
||
const ersteRate = page.locator('table.liste').last().locator('tbody tr').first()
|
||
await expect(ersteRate).toContainText('250,00')
|
||
|
||
page.once('dialog', dialog => dialog.accept())
|
||
await page.locator('tr', { hasText: 'E2E-Darlehen' }).locator('button:has-text("Löschen")').first().click()
|
||
await expect(page.locator('.meldung')).toContainText('gelöscht')
|
||
})
|
||
|
||
test('Der Finanzkalender ist erreichbar und lässt sich einstellen', async ({ page }) => {
|
||
await page.goto('/termine/kalender')
|
||
await expect(page.locator('h1')).toContainText('Finanzkalender')
|
||
|
||
await page.selectOption('#tage', '90')
|
||
await page.click('button:has-text("Anzeigen")')
|
||
await expect(page).toHaveURL(/tage=90/)
|
||
})
|
||
|
||
test('Der Zahlungsverkehr weist eine falsche IBAN ab', async ({ page }) => {
|
||
await page.goto('/zahlungsverkehr/neu')
|
||
|
||
await page.fill('#empfaenger', 'E2E-Empfänger')
|
||
await page.fill('#iban', 'DE00000000000000000000')
|
||
await page.fill('#betrag', '10,00')
|
||
await page.click('button:has-text("Erfassen")')
|
||
|
||
// Die Bankverbindung ist bewusst nicht Pflicht: Ein Auftrag darf erfasst
|
||
// werden, bevor eine eingerichtet ist. Geprüft wird sie erst beim Senden.
|
||
await expect(page.locator('.meldung-fehler')).toContainText('IBAN')
|
||
})
|
||
})
|