Quelltext des aktuellen Standes
<?php
declare(strict_types=1);
use OpenNIT\Content\CostOverview;
use OpenNIT\Core\Database;
/**
* Deckt die Zusammenfuehrung der laufenden IT-Kosten ab.
*
* Der heikle Punkt ist die Doppelzaehlung: Eine Lizenz, die einem Vertrag
* zugeordnet ist, wird ueber diesen Vertrag abgerechnet. Ihr Betrag steckt also
* bereits in den Vertragskosten und darf nicht noch einmal in die Summe - er
* darf aber auch nicht stillschweigend verschwinden.
*/
return static function (): void {
/**
* Datenbank-Attrappe mit vorgegebenen Vertraegen und Lizenzen.
*
* @param list<array<string,mixed>> $vertraege
* @param list<array<string,mixed>> $lizenzen
*/
$db = static function (array $vertraege, array $lizenzen): Database {
return new class ($vertraege, $lizenzen) extends Database {
public function __construct(private array $vertraege, private array $lizenzen) {}
public function fetchAll(string $sql, array $params = []): array
{
return str_contains($sql, 'FROM contracts') ? $this->vertraege : $this->lizenzen;
}
};
};
/** Kurzform fuer einen Vertrag. */
$vertrag = static fn (string $titel, float $betrag, string $intervall, string $kat = 'Allgemein'): array =>
['id' => 1, 'title' => $titel, 'cost_amount' => $betrag, 'cost_interval' => $intervall,
'status' => 'active', 'kategorie' => $kat];
/** Kurzform fuer eine Lizenz; $vertraege = Zahl zugeordneter Vertraege. */
$lizenz = static fn (string $name, float $betrag, string $intervall, int $vertraege = 0): array =>
['id' => 1, 'name' => $name, 'vendor' => 'Hersteller', 'cost_amount' => $betrag,
'cost_interval' => $intervall, 'vertraege' => $vertraege];
// ----------------------------------------------------------------
// Umrechnung
// ----------------------------------------------------------------
test('Jedes Intervall wird auf den Monat gerechnet', function () {
assertSame(100.0, CostOverview::monatsbetrag(100.0, 'monthly'));
assertSame(100.0, CostOverview::monatsbetrag(300.0, 'quarterly'));
assertSame(100.0, CostOverview::monatsbetrag(1200.0, 'yearly'));
});
test('Eine einmalige Zahlung ergibt keine laufenden Kosten', function () {
// Sonst erschiene eine Anschaffung als monatliche Belastung.
assertSame(0.0, CostOverview::monatsbetrag(5000.0, 'one_time'));
assertSame(0.0, CostOverview::monatsbetrag(100.0, null));
assertSame(0.0, CostOverview::monatsbetrag(100.0, 'gibtesnicht'));
});
test('Fehlende und unsinnige Betraege zaehlen nicht', function () {
assertSame(0.0, CostOverview::monatsbetrag(null, 'monthly'));
assertSame(0.0, CostOverview::monatsbetrag(0.0, 'monthly'));
assertSame(0.0, CostOverview::monatsbetrag(-50.0, 'monthly'));
});
// ----------------------------------------------------------------
// Doppelzaehlung
// ----------------------------------------------------------------
test('Eine Lizenz mit Vertrag geht nicht in die Summe', function () use ($db, $vertrag, $lizenz) {
$u = (new CostOverview($db(
[$vertrag('Wartung', 1200.0, 'yearly')], // 100/Monat
[$lizenz('Office', 50.0, 'monthly', 1)] // ueber Vertrag
)))->zusammenfassung(true, true);
assertSame(100.0, $u['monatlich'], 'Die Lizenz wurde doppelt gezaehlt');
assertSame(1200.0, $u['jaehrlich']);
assertSame(0.0, $u['lizenzen_monatlich']);
});
test('Eine Lizenz ohne Vertrag zaehlt mit', function () use ($db, $vertrag, $lizenz) {
$u = (new CostOverview($db(
[$vertrag('Wartung', 1200.0, 'yearly')],
[$lizenz('Office', 50.0, 'monthly', 0)]
)))->zusammenfassung(true, true);
assertSame(150.0, $u['monatlich']);
assertSame(50.0, $u['lizenzen_monatlich']);
assertSame(100.0, $u['vertraege_monatlich']);
});
test('Mehrere Vertraege an einer Lizenz aendern nichts', function () use ($db, $vertrag, $lizenz) {
// Die Zuordnung ist n:m - eine einzige genuegt fuer "abgedeckt".
$u = (new CostOverview($db(
[$vertrag('Wartung', 100.0, 'monthly')],
[$lizenz('Office', 50.0, 'monthly', 3)]
)))->zusammenfassung(true, true);
assertSame(100.0, $u['monatlich']);
assertSame(1, $u['abgedeckt_anzahl']);
});
test('Die ausgelassenen Lizenzen werden beziffert', function () use ($db, $vertrag, $lizenz) {
// Eine stillschweigend weggelassene Position faellt niemandem auf.
$u = (new CostOverview($db(
[$vertrag('Wartung', 100.0, 'monthly')],
[$lizenz('A', 60.0, 'monthly', 1), $lizenz('B', 120.0, 'yearly', 1), $lizenz('C', 10.0, 'monthly', 0)]
)))->zusammenfassung(true, true);
assertSame(2, $u['abgedeckt_anzahl'], 'Nicht alle ausgelassenen gezaehlt');
assertSame(70.0, $u['abgedeckt_monatlich'], 'Der ausgelassene Betrag stimmt nicht');
assertSame(110.0, $u['monatlich'], 'Nur die freie Lizenz gehoert dazu');
});
// ----------------------------------------------------------------
// Abgeschaltete Module
// ----------------------------------------------------------------
test('Ohne Lizenz-Modul bleiben die Vertraege unveraendert', function () use ($db, $vertrag, $lizenz) {
$u = (new CostOverview($db(
[$vertrag('Wartung', 100.0, 'monthly')],
[$lizenz('Office', 999.0, 'monthly', 0)]
)))->zusammenfassung(true, false);
assertSame(100.0, $u['monatlich'], 'Lizenzen flossen trotz abgeschaltetem Modul ein');
assertSame([], $u['lizenzen']);
});
test('Ganz ohne Daten steht ueberall null', function () use ($db) {
$u = (new CostOverview($db([], [])))->zusammenfassung(true, true);
assertSame(0.0, $u['monatlich']);
assertSame(0.0, $u['jaehrlich']);
assertSame(0.0, $u['einmalig']);
assertSame([], $u['nach_kategorie']);
});
// ----------------------------------------------------------------
// Einmalige Zahlungen und Gruppierung
// ----------------------------------------------------------------
test('Einmalige Zahlungen stehen gesondert', function () use ($db, $vertrag, $lizenz) {
$u = (new CostOverview($db(
[$vertrag('Wartung', 100.0, 'monthly'), $vertrag('Anschaffung', 5000.0, 'one_time')],
[$lizenz('Dauerlizenz', 800.0, 'one_time', 0)]
)))->zusammenfassung(true, true);
assertSame(100.0, $u['monatlich'], 'Einmaliges floss in die laufenden Kosten');
assertSame(5800.0, $u['einmalig']);
});
test('Die Kategorien summieren sich zur Monatssumme', function () use ($db, $vertrag, $lizenz) {
$u = (new CostOverview($db(
[$vertrag('A', 100.0, 'monthly', 'Software'), $vertrag('B', 60.0, 'monthly', 'Hardware'),
$vertrag('C', 120.0, 'yearly', 'Software')],
[$lizenz('Frei', 30.0, 'monthly', 0)]
)))->zusammenfassung(true, true);
assertSame(200.0, $u['monatlich']);
assertSame(200.0, array_sum($u['nach_kategorie']), 'Die Aufteilung ergibt nicht die Summe');
// Absteigend, damit der groesste Posten oben steht.
assertSame(['Software', 'Hardware', 'Lizenzen (ohne Vertrag)'], array_keys($u['nach_kategorie']));
});
test('Abgedeckte Lizenzen tauchen nicht in den Kategorien auf', function () use ($db, $vertrag, $lizenz) {
$u = (new CostOverview($db(
[$vertrag('A', 100.0, 'monthly', 'Software')],
[$lizenz('Abgedeckt', 50.0, 'monthly', 1)]
)))->zusammenfassung(true, true);
assertFalse(isset($u['nach_kategorie']['Lizenzen (ohne Vertrag)']), 'Abgedeckte Lizenz in der Aufteilung');
assertSame(100.0, array_sum($u['nach_kategorie']));
});
// ----------------------------------------------------------------
// Auswahl der Datensaetze
// ----------------------------------------------------------------
test('Nur laufende Vertraege und aktive Lizenzen', function () {
$quelle = (string)file_get_contents(dirname(__DIR__) . '/src/Content/CostOverview.php');
assertTrue(str_contains($quelle, "['active', 'expiring']"), 'Vertragszustaende geaendert');
assertTrue(str_contains($quelle, "l.status = 'active'"), 'Lizenzzustand geaendert');
// Ohne Betrag gibt es nichts zu rechnen.
assertSame(2, substr_count($quelle, 'cost_amount > 0'), 'Betragsfilter fehlt');
});
test('Die Seite schreibt nichts', function () {
$quelle = (string)file_get_contents(dirname(__DIR__) . '/src/Content/CostOverview.php');
foreach (['INSERT', 'UPDATE ', 'DELETE', '->insert(', '->update(', '->execute('] as $wort) {
assertFalse(str_contains($quelle, $wort), "Schreibender Zugriff: {$wort}");
}
});
};