Steuerkram/tests/82_AfaBuchungModellTest.php
fds2610-bot 88d9cc53ff Fix #155: AfA-Buchungen laufen jetzt über Booking::createAfa()/reverseAfa()
AssetController setzte INSERT INTO bookings bislang direkt ab und umging
damit die Sperre des Geschäftsjahres, den transaktionalen Schutz der
Nummernvergabe und ordnete Stornos dem falschen Geschäftsjahr zu.
2026-08-03 22:09:16 +02:00

163 lines
6 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
declare(strict_types=1);
use Steuerkram\Models\Asset;
use Steuerkram\Models\Booking;
use Steuerkram\Models\FiscalYear;
/**
* Test 82: AfA-Buchungen laufen über Booking::createAfa()/reverseAfa().
*
* Fix #155: AssetController setzte INSERT INTO bookings bislang direkt ab und
* umging damit die Sperre des Geschäftsjahres, den Transaktionsschutz der
* Nummernvergabe und – beim Storno – die richtige Jahreszuordnung.
*/
class AfaBuchungModellTest extends TestCase
{
protected string $testName = 'Test 82: AfA-Buchungen über Booking-Modell';
private int $assetId;
private int $kategorieId;
private int $zahlungsId;
public function run(): void
{
$this->kategorieId = (int)$this->findAccountId('4855');
$this->zahlungsId = (int)$this->findAccountId('0420');
$this->assetId = Asset::create([
'bezeichnung' => 'Testgerät AfA-Modell',
'anschaffungsdatum' => '2024-01-15',
'anschaffungskosten' => 1200.00,
'zugaenge' => 0,
'abgaenge' => 0,
'abschreibung_jahr' => 400.00,
'nutzungsdauer' => 3,
'account_id' => $this->zahlungsId,
]);
$this->gesperrtesJahrLehntAfaBuchungAb();
$this->afaBuchungInOffenemJahr();
$this->stornoUebernimmtJahrDerOriginalbuchung();
$this->gesperrtesJahrLehntStornoAb();
}
private function gesperrtesJahrLehntAfaBuchungAb(): void
{
$jahr = 2019;
$fy = FiscalYear::findOrCreateByYear($jahr);
\Steuerkram\Database::execute('UPDATE fiscal_years SET locked = 1 WHERE id = ?', [$fy['id']]);
$vorher = $this->countBookings();
$exception = null;
try {
Booking::createAfa($this->afaData($jahr));
} catch (\RuntimeException $e) {
$exception = $e;
}
$this->assert($exception !== null, 'AfA-Buchung in gesperrtem Geschäftsjahr wirft eine RuntimeException');
$this->assertEquals(
$vorher,
$this->countBookings(),
'Bei Sperre wird kein Datensatz in bookings angelegt'
);
}
private function afaBuchungInOffenemJahr(): void
{
$jahr = 2024;
$bookingId = Booking::createAfa($this->afaData($jahr));
$this->assertGreaterThan(0, $bookingId, 'AfA-Buchung in offenem Jahr wird angelegt');
$booking = Booking::find($bookingId);
$this->assertEquals($jahr, (int)$booking['afa_jahr'], 'afa_jahr ist gesetzt');
$this->assertEquals($jahr . '-12-31', $booking['buchungs_datum'], 'buchungs_datum liegt auf dem 31.12. des AfA-Jahres');
$this->assertEquals(0.00, (float)$booking['betrag_ust'], 'betrag_ust ist 0');
$this->assertEquals(400.00, (float)$booking['betrag_netto'], 'betrag_netto entspricht dem AfA-Betrag');
$this->assert(
(bool)preg_match('/^AFA-\d{5}$/', $booking['buchungs_nr']),
'Buchungsnummer folgt dem Schema AFA-NNNNN (erhalten: ' . $booking['buchungs_nr'] . ')'
);
}
private function stornoUebernimmtJahrDerOriginalbuchung(): void
{
// Vorjahr: 2022 – das laufende Kalenderjahr ist ein anderes.
$jahr = 2022;
$bookingId = Booking::createAfa($this->afaData($jahr));
$original = Booking::find($bookingId);
$reversalId = Booking::reverseAfa($bookingId);
$reversal = Booking::find($reversalId);
$this->assertEquals(
$original['fiscal_year_id'],
$reversal['fiscal_year_id'],
'Storno erhält die fiscal_year_id der Originalbuchung, nicht die des aktuellen Jahres'
);
$aktuellesJahr = FiscalYear::findOrCreateByYear((int)date('Y'));
$this->assert(
(int)$reversal['fiscal_year_id'] !== (int)$aktuellesJahr['id'],
'fiscal_year_id des Storno ist nicht die des laufenden Kalenderjahres'
);
$this->assertEquals(1, (int)$reversal['is_reversal'], 'Stornobuchung ist als is_reversal=1 markiert');
$updatedOriginal = Booking::find($bookingId);
$this->assertEquals(
$reversalId,
(int)$updatedOriginal['reversed_by'],
'reversed_by der Originalbuchung zeigt auf die Stornobuchung'
);
$this->assert(
(bool)preg_match('/^AFA-\d{5}$/', $reversal['buchungs_nr']),
'Storno-Buchungsnummer folgt ebenfalls dem Schema AFA-NNNNN'
);
}
private function gesperrtesJahrLehntStornoAb(): void
{
$jahr = 2020;
$bookingId = Booking::createAfa($this->afaData($jahr));
$fy = FiscalYear::findByYear($jahr);
\Steuerkram\Database::execute('UPDATE fiscal_years SET locked = 1 WHERE id = ?', [$fy['id']]);
$vorher = $this->countBookings();
$exception = null;
try {
Booking::reverseAfa($bookingId);
} catch (\RuntimeException $e) {
$exception = $e;
}
$this->assert($exception !== null, 'Storno einer AfA-Buchung im gesperrten Jahr wirft eine RuntimeException');
$this->assertEquals(
$vorher,
$this->countBookings(),
'Bei Sperre wird kein zusätzlicher Storno-Datensatz angelegt'
);
$unverändert = Booking::find($bookingId);
$this->assert(empty($unverändert['reversed_by']), 'Originalbuchung bleibt ohne reversed_by, wenn das Storno abgelehnt wird');
}
private function afaData(int $jahr): array
{
return [
'asset_id' => $this->assetId,
'afa_jahr' => $jahr,
'kategorie_konto_id' => $this->kategorieId,
'zahlungskonto_id' => $this->zahlungsId,
'betrag' => 400.00,
'buchungstext' => 'AfA Test ' . $jahr,
];
}
private function countBookings(): int
{
$row = \Steuerkram\Database::fetchOne('SELECT COUNT(*) as cnt FROM bookings');
return (int)($row['cnt'] ?? 0);
}
}