findAccountId('8400'); // Betriebseinnahme $buerobedarf = $this->findAccountId('4930'); // Betriebsausgabe $bewirtung = $this->findAccountId('4654'); // Betriebsausgabe, euer_zeile 74 $vorsteuer = $this->findAccountId('1576'); // Betriebsausgabe, euer_zeile 57 $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 – Betriebsausgabe nach EÜR-Zeile 57 (Kz 185) $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'] + 338.00, 2), $nachher['ausgaben'], 'Ausgaben enthalten Aufwand, AfA und Vorsteuer, aber keine nicht abzugsfähigen 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'] + 662.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" ); } // Vorsteuer gehört in die Ausgabenliste, nicht abzugsfähige Kosten nicht $kontonummern = array_column($calc->fetchBookingsForBucket($jahr, null, 'ausgaben'), 'kategorie_nr'); $this->assert(in_array('1576', $kontonummern, true), 'Vorsteuerkonto steht 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', ] ); } }