200 lines
8.3 KiB
PHP
200 lines
8.3 KiB
PHP
<?php
|
||
|
||
declare(strict_types=1);
|
||
|
||
use Steuerkram\Database;
|
||
use Steuerkram\Services\EuerCalculator;
|
||
|
||
/**
|
||
* Test 80: Dashboard-Kacheln stimmen mit der EÜR überein (Issue #167)
|
||
*
|
||
* Das Dashboard summierte bisher nach bookings.buchungstyp, die EÜR nach
|
||
* accounts.kontenart. Dadurch zählten Vorsteuer- und Anlagenbuchungen als
|
||
* Betriebsausgabe, AfA-Buchungen fielen ganz heraus und nicht abzugsfähige
|
||
* Kosten minderten das angezeigte Ergebnis. Geprüft wird, dass beide
|
||
* Auswertungen jetzt dieselbe Zuordnung verwenden und dass die Detailliste
|
||
* hinter einer Kachel exakt auf deren Summe kommt.
|
||
*/
|
||
class DashboardEuerAbgleichTest extends TestCase
|
||
{
|
||
protected string $testName = 'Test 80: Dashboard-Kacheln und EÜR liefern dieselben Summen';
|
||
|
||
public function run(): void
|
||
{
|
||
$calc = new EuerCalculator();
|
||
$jahr = (int)date('Y');
|
||
|
||
$erloese = $this->findAccountId('8400'); // Betriebseinnahme
|
||
$buerobedarf = $this->findAccountId('4930'); // Betriebsausgabe
|
||
$bewirtung = $this->findAccountId('4654'); // Betriebsausgabe, euer_zeile 74
|
||
$vorsteuer = $this->findAccountId('1576'); // Zahlungskonto
|
||
$bank = $this->findAccountId('1200'); // Zahlungskonto
|
||
|
||
$this->assertNotEmpty($erloese, 'Erlöskonto 8400 vorhanden');
|
||
$this->assertNotEmpty($buerobedarf, 'Aufwandskonto 4930 vorhanden');
|
||
$this->assertNotEmpty($bewirtung, 'Bewirtungskonto 4654 vorhanden');
|
||
$this->assertNotEmpty($vorsteuer, 'Vorsteuerkonto 1576 vorhanden');
|
||
$this->assertNotEmpty($bank, 'Bankkonto 1200 vorhanden');
|
||
|
||
$vorher = $calc->summary($jahr);
|
||
|
||
$belegId = $this->createBeleg();
|
||
$monat = 6;
|
||
$datum = sprintf('%04d-%02d-15', $jahr, $monat);
|
||
|
||
// Einnahme 1.000,00 – zählt in beiden Auswertungen
|
||
$this->createBooking($belegId, $datum, 'Einnahme', $erloese, $bank, 1000.00);
|
||
// Betriebsausgabe 200,00 – zählt in beiden Auswertungen
|
||
$this->createBooking($belegId, $datum, 'Ausgabe', $buerobedarf, $bank, 200.00);
|
||
// AfA 100,00 – fiel im alten Dashboard komplett unter den Tisch
|
||
$this->createBooking($belegId, $datum, 'AfA', $buerobedarf, $bank, 100.00);
|
||
// Vorsteuer 38,00 – wurde im alten Dashboard als Ausgabe mitgezählt
|
||
$this->createBooking($belegId, $datum, 'Ausgabe', $vorsteuer, $bank, 38.00);
|
||
// Nicht abzugsfähige Bewirtung 30,00 – mindert das Ergebnis nicht
|
||
$this->createBooking($belegId, $datum, 'Ausgabe', $bewirtung, $bank, 30.00);
|
||
|
||
$nachher = $calc->summary($jahr);
|
||
|
||
$this->assertEquals(
|
||
round($vorher['einnahmen'] + 1000.00, 2),
|
||
$nachher['einnahmen'],
|
||
'Einnahmen steigen um genau den Erlös'
|
||
);
|
||
$this->assertEquals(
|
||
round($vorher['ausgaben'] + 300.00, 2),
|
||
$nachher['ausgaben'],
|
||
'Ausgaben enthalten Aufwand und AfA, aber weder Vorsteuer noch nicht abzugsfähige Kosten'
|
||
);
|
||
$this->assertEquals(
|
||
round($vorher['nicht_abzugsfaehig'] + 30.00, 2),
|
||
$nachher['nicht_abzugsfaehig'],
|
||
'Nicht abzugsfähige Kosten werden getrennt ausgewiesen'
|
||
);
|
||
$this->assertEquals(
|
||
round($vorher['ergebnis'] + 700.00, 2),
|
||
$nachher['ergebnis'],
|
||
'Ergebnis = Einnahmen − Ausgaben'
|
||
);
|
||
|
||
// Deckungsgleich mit der vollständigen EÜR-Berechnung
|
||
$euer = $calc->calculate($jahr);
|
||
$this->assertEquals($euer['total_einnahmen'], $nachher['einnahmen'], 'Einnahmen wie in der EÜR');
|
||
$this->assertEquals($euer['total_ausgaben'], $nachher['ausgaben'], 'Ausgaben wie in der EÜR');
|
||
$this->assertEquals($euer['gewinn'], $nachher['ergebnis'], 'Ergebnis wie in der EÜR');
|
||
|
||
// Detaillisten summieren auf den Kachelwert
|
||
foreach (['einnahmen', 'ausgaben', 'ergebnis'] as $bucket) {
|
||
$bookings = $calc->fetchBookingsForBucket($jahr, null, $bucket);
|
||
$summe = 0.0;
|
||
foreach ($bookings as $b) {
|
||
$summe += ($bucket === 'ergebnis' && $b['kontenart'] === 'Betriebsausgabe')
|
||
? -(float)$b['betrag_netto']
|
||
: (float)$b['betrag_netto'];
|
||
}
|
||
$this->assertEquals(
|
||
$nachher[$bucket === 'ergebnis' ? 'ergebnis' : $bucket],
|
||
round($summe, 2),
|
||
"Detailliste '{$bucket}' summiert auf den Kachelwert"
|
||
);
|
||
}
|
||
|
||
// Keine Vorsteuer- und keine nicht abzugsfähige Buchung in der Ausgabenliste
|
||
$kontonummern = array_column($calc->fetchBookingsForBucket($jahr, null, 'ausgaben'), 'kategorie_nr');
|
||
$this->assert(!in_array('1576', $kontonummern, true), 'Vorsteuerkonto fehlt in der Ausgabenliste');
|
||
$this->assert(!in_array('4654', $kontonummern, true), 'Nicht abzugsfähige Bewirtung fehlt in der Ausgabenliste');
|
||
|
||
// Monatsfilter greift
|
||
$juni = $calc->summary($jahr, $monat);
|
||
$this->assert($juni['einnahmen'] >= 1000.00, 'Monatsauswertung enthält die Junibuchung');
|
||
$andererMonat = $calc->summary($jahr, $monat === 12 ? 1 : $monat + 1);
|
||
$this->assert(
|
||
$andererMonat['einnahmen'] < $juni['einnahmen'],
|
||
'Ein anderer Monat enthält die Junibuchung nicht'
|
||
);
|
||
|
||
// Unbekannte Gruppe wird abgewiesen
|
||
$abgewiesen = false;
|
||
try {
|
||
$calc->fetchBookingsForBucket($jahr, null, 'unsinn');
|
||
} catch (\InvalidArgumentException) {
|
||
$abgewiesen = true;
|
||
}
|
||
$this->assert($abgewiesen, 'Unbekannte Auswertungsgruppe löst eine Ausnahme aus');
|
||
|
||
// Route und Controller-Action vorhanden
|
||
$routes = (string)file_get_contents(__DIR__ . '/../public/index.php');
|
||
$this->assert(
|
||
str_contains($routes, '/dashboard/buchungen/{bucket}/{year}')
|
||
&& str_contains($routes, '/dashboard/buchungen/{bucket}/{year}/{month}'),
|
||
'Routen für Jahres- und Monatsdetails sind registriert'
|
||
);
|
||
|
||
$dashboard = (string)file_get_contents(__DIR__ . '/../src/Views/dashboard.php');
|
||
$this->assert(
|
||
substr_count($dashboard, '/dashboard/buchungen/') >= 1 && str_contains($dashboard, "\$detail("),
|
||
'Die Kachelzahlen verlinken auf die Detailansicht'
|
||
);
|
||
$this->assertEquals(
|
||
9,
|
||
substr_count($dashboard, '$detail('),
|
||
'Alle neun Zahlen der ersten drei Kacheln sind verlinkt'
|
||
);
|
||
}
|
||
|
||
private function createBeleg(): int
|
||
{
|
||
Database::execute(
|
||
'INSERT INTO belege (beleg_nr, filename, storage_path, mime_type, file_hash, beleg_datum, belegtyp)
|
||
VALUES (?, ?, ?, ?, ?, ?, ?)',
|
||
[
|
||
'TST-DASH-' . substr(bin2hex(random_bytes(4)), 0, 8),
|
||
'dashboard-test.pdf',
|
||
'test/dashboard-test.pdf',
|
||
'application/pdf',
|
||
hash('sha256', uniqid('dash', true)),
|
||
date('Y-m-d'),
|
||
'Eingangsrechnung',
|
||
]
|
||
);
|
||
return (int)Database::lastInsertId();
|
||
}
|
||
|
||
private function createBooking(
|
||
int $belegId,
|
||
string $datum,
|
||
string $typ,
|
||
int $kategorieKonto,
|
||
int $zahlungsKonto,
|
||
float $netto
|
||
): void {
|
||
$fy = Database::fetchOne('SELECT id FROM fiscal_years WHERE year = ?', [(int)date('Y', strtotime($datum))]);
|
||
if (!$fy) {
|
||
Database::execute('INSERT INTO fiscal_years (year) VALUES (?)', [(int)date('Y', strtotime($datum))]);
|
||
$fiscalYearId = (int)Database::lastInsertId();
|
||
} else {
|
||
$fiscalYearId = (int)$fy['id'];
|
||
}
|
||
|
||
Database::execute(
|
||
'INSERT INTO bookings
|
||
(beleg_id, fiscal_year_id, buchungs_nr, buchungs_datum, buchungstyp,
|
||
kategorie_konto_id, zahlungskonto_id, ust_satz, betrag_netto, betrag_ust,
|
||
betrag_brutto, buchungstext)
|
||
VALUES (?,?,?,?,?,?,?,?,?,?,?,?)',
|
||
[
|
||
$belegId,
|
||
$fiscalYearId,
|
||
'TST-' . substr(bin2hex(random_bytes(5)), 0, 10),
|
||
$datum,
|
||
$typ,
|
||
$kategorieKonto,
|
||
$zahlungsKonto,
|
||
0,
|
||
$netto,
|
||
0,
|
||
$netto,
|
||
'Testbuchung Dashboard-Abgleich',
|
||
]
|
||
);
|
||
}
|
||
}
|