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.
163 lines
6 KiB
PHP
163 lines
6 KiB
PHP
<?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);
|
||
}
|
||
}
|