Steuerkram/tests/78_NummernkreisFreigabeTest.php

187 lines
7.6 KiB
PHP
Raw Permalink 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);
/**
* Test 78: Freigabe der zuletzt vergebenen Nummer beim Löschen.
*
* Wird eine Rechnung oder ein Beleg gelöscht, gibt der jeweilige Nummernkreis
* die Nummer wieder frei – aber nur, wenn es die zuletzt vergebene war. Sonst
* hinterließe jeder Löschvorgang (z.B. ein E2E-Testlauf) eine unumkehrbare
* Lücke im Nummernkreis.
*
* Gegenprobe: Wird eine ältere Nummer gelöscht, bleibt der Zähler stehen –
* andernfalls würde eine bereits vergebene Nummer ein zweites Mal ausgegeben.
*/
class NummernkreisFreigabeTest extends TestCase
{
protected string $testName = 'Test 78: Nummernkreis gibt die zuletzt vergebene Nummer beim Löschen frei';
public function run(): void
{
$this->nummernkreisAufAktuellenMonat();
$this->rechnungsnummerWirdFreigegeben();
$this->aeltereRechnungsnummerBleibtVerbraucht();
$this->belegnummerWirdFreigegeben();
}
// ── Rechnungen ──────────────────────────────────────────────────────────
private function rechnungsnummerWirdFreigegeben(): void
{
$vorher = $this->invoiceNrNext();
$invoiceId = $this->createRechnung('Nummernkreis-Freigabe');
$nr = \Steuerkram\Models\Invoice::finalize($invoiceId);
$this->assertNotEmpty($nr, 'Finalisieren vergibt eine Rechnungsnummer');
$this->assertEquals($vorher + 1, $this->invoiceNrNext(), 'Zähler steht nach dem Finalisieren eins weiter');
\Steuerkram\Models\Invoice::delete($invoiceId);
$this->assertEquals(
$vorher,
$this->invoiceNrNext(),
'Nach dem Löschen steht der Zähler wieder auf dem Ausgangswert'
);
// Die freigegebene Nummer muss erneut vergeben werden – sonst entstünde
// trotz zurückgesetztem Zähler eine andere Nummer.
$zweiteId = $this->createRechnung('Nummernkreis-Wiedervergabe');
$this->assertEquals($nr, \Steuerkram\Models\Invoice::finalize($zweiteId), 'Die freigegebene Nummer wird erneut vergeben');
\Steuerkram\Models\Invoice::delete($zweiteId);
}
private function aeltereRechnungsnummerBleibtVerbraucht(): void
{
$ersteId = $this->createRechnung('Nummernkreis-Alt');
\Steuerkram\Models\Invoice::finalize($ersteId);
$zweiteId = $this->createRechnung('Nummernkreis-Neu');
$zweiteNr = \Steuerkram\Models\Invoice::finalize($zweiteId);
$standMitBeiden = $this->invoiceNrNext();
// Die ältere der beiden Rechnungen löschen: ihre Nummer darf nicht
// freigegeben werden, sie liegt vor der bereits vergebenen $zweiteNr.
\Steuerkram\Models\Invoice::delete($ersteId);
$this->assertEquals(
$standMitBeiden,
$this->invoiceNrNext(),
'Das Löschen einer älteren Rechnung lässt den Zähler unverändert'
);
// Jetzt die jüngste löschen – deren Nummer wird freigegeben.
\Steuerkram\Models\Invoice::delete($zweiteId);
$this->assertEquals(
$standMitBeiden - 1,
$this->invoiceNrNext(),
'Das Löschen der jüngsten Rechnung gibt deren Nummer frei'
);
$dritteId = $this->createRechnung('Nummernkreis-Nachrueck');
$this->assertEquals(
$zweiteNr,
\Steuerkram\Models\Invoice::finalize($dritteId),
'Die freigegebene Nummer der jüngsten Rechnung rückt nach'
);
\Steuerkram\Models\Invoice::delete($dritteId);
}
// ── Belege ──────────────────────────────────────────────────────────────
private function belegnummerWirdFreigegeben(): void
{
$vorher = $this->belegNrNext();
$ersterId = $this->createBeleg('Nummernkreis-Beleg-1');
$zweiterId = $this->createBeleg('Nummernkreis-Beleg-2');
$this->assertEquals($vorher + 2, $this->belegNrNext(), 'Zwei Belege verbrauchen zwei Nummern');
// Älterer Beleg: keine Freigabe.
$this->assert(\Steuerkram\Models\Beleg::delete($ersterId), 'Älterer Beleg ist löschbar');
$this->assertEquals(
$vorher + 2,
$this->belegNrNext(),
'Das Löschen eines älteren Belegs lässt den Zähler unverändert'
);
// Jüngster Beleg: Nummer wird frei.
$this->assert(\Steuerkram\Models\Beleg::delete($zweiterId), 'Jüngster Beleg ist löschbar');
$this->assertEquals(
$vorher + 1,
$this->belegNrNext(),
'Das Löschen des jüngsten Belegs gibt dessen Nummer frei'
);
}
// ── Hilfsfunktionen ─────────────────────────────────────────────────────
/**
* Setzt den gespeicherten Monat des Rechnungsnummernkreises auf den
* laufenden Monat.
*
* Ohne das misst der Test am Monatswechsel vorbei: Steht in
* company_settings noch ein früherer Monat, beginnt generateRechnungsNr()
* den Zähler planmäßig wieder bei 1, und die Deltas dieses Tests
* (vorher + 1, danach wieder vorher) gehen nicht auf. Der Monatsreset ist
* gewolltes Verhalten und hat mit der hier geprüften Freigabe nichts zu
* tun. Die Änderung fällt mit dem Transaktions-Rollback wieder weg.
*/
private function nummernkreisAufAktuellenMonat(): void
{
\Steuerkram\Database::execute(
'UPDATE company_settings SET invoice_nr_year=?, invoice_nr_month=? WHERE id=1',
[(int)date('Y'), (int)date('n')]
);
}
private function invoiceNrNext(): int
{
$row = \Steuerkram\Database::fetchOne('SELECT invoice_nr_next FROM company_settings WHERE id=1');
return (int)($row['invoice_nr_next'] ?? 0);
}
private function belegNrNext(): int
{
$row = \Steuerkram\Database::fetchOne('SELECT beleg_nr_next FROM company_settings WHERE id=1');
return (int)($row['beleg_nr_next'] ?? 0);
}
private function createRechnung(string $marker): int
{
$customerId = \Steuerkram\Models\Customer::create([
'nachname' => 'Nummernkreis',
'firma' => $marker . ' GmbH',
'strasse' => 'Zählerweg 1',
'plz' => '12345',
'ort' => 'Nummernstadt',
'land' => 'DE',
'payment_days' => 14,
]);
return \Steuerkram\Models\Invoice::create([
'customer_id' => $customerId,
'betreff' => $marker,
'rechnungs_datum' => date('Y-m-d'),
'lines' => [
['bezeichnung' => 'Leistung', 'menge' => 1, 'einheit' => 'Std.', 'einzelpreis' => 100.00, 'ust_satz' => 19.0],
],
]);
}
/** Legt einen Beleg ohne Datei an (storage_path zeigt bewusst ins Leere). */
private function createBeleg(string $marker): int
{
$belegNr = \Steuerkram\Models\Beleg::generateBelegNr();
return \Steuerkram\Models\Beleg::create([
'beleg_nr' => $belegNr,
'filename' => $marker . '.pdf',
'storage_path' => date('Y') . '/' . $marker . '/original.pdf',
'mime_type' => 'application/pdf',
'file_hash' => hash('sha256', $belegNr . $marker),
'belegtyp' => 'Eingangsrechnung',
'beleg_datum' => date('Y-m-d'),
'notes' => $marker,
]);
}
}