187 lines
7.6 KiB
PHP
187 lines
7.6 KiB
PHP
<?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,
|
||
]);
|
||
}
|
||
}
|