OpenNIT

Quelltext des aktuellen Standes

Fassung 1.1.1

AssetValuationTest.php

13 KB
<?php

declare(strict_types=1);

use OpenNIT\Assets\Valuation;

/**
 * Deckt Nutzungsdauer, Abschreibung und Restwert im Inventar ab.
 *
 * Drei Punkte sind hier bindend: Die drei Angaben lassen sich getrennt
 * zuschalten (die Nutzungsdauer insbesondere ohne jeden Betrag), ohne
 * Grundlage entsteht kein geschätzter Wert, und die Summe aus Restwert und
 * bereits abgeschriebenem Anteil bleibt der Anschaffungswert.
 */
return static function (): void {

    // ----------------------------------------------------------------
    // Schalter
    // ----------------------------------------------------------------

    test('Ohne Einstellung ist keine der drei Angaben in Gebrauch', function () {
        $v = Valuation::ausWerten([]);
        assertFalse($v->nutzungsdauer);
        assertFalse($v->abschreibung);
        assertFalse($v->restwert);
        assertFalse($v->aktiv());
        assertFalse($v->brauchtNutzungsdauer());
        assertFalse($v->brauchtAnschaffungswert());
    });

    test('Die Nutzungsdauer steht allein, ohne Anschaffungswert', function () {
        $v = Valuation::ausWerten(['assets_useful_life_enabled' => '1']);
        assertTrue($v->nutzungsdauer);
        assertFalse($v->abschreibung);
        assertFalse($v->restwert);
        assertTrue($v->aktiv());
        assertTrue($v->brauchtNutzungsdauer());
        // Für eine reine Ersatzplanung wird kein Betrag verlangt.
        assertFalse($v->brauchtAnschaffungswert());
    });

    test('Abschreibung und Restwert brauchen einander nicht', function () {
        $nurAfa = Valuation::ausWerten(['assets_depreciation_enabled' => '1']);
        assertTrue($nurAfa->abschreibung);
        assertFalse($nurAfa->restwert);
        assertTrue($nurAfa->brauchtAnschaffungswert());
        assertTrue($nurAfa->brauchtNutzungsdauer());

        $nurRest = Valuation::ausWerten(['assets_residual_value_enabled' => '1']);
        assertTrue($nurRest->restwert);
        assertFalse($nurRest->abschreibung);
        assertTrue($nurRest->brauchtAnschaffungswert());
        assertTrue($nurRest->brauchtNutzungsdauer());
    });

    test('Ein unbekannter Rechenweg fällt auf die monatsgenaue Methode zurück', function () {
        assertSame(
            Valuation::METHODE_MONATLICH,
            Valuation::ausWerten(['assets_depreciation_method' => 'degressiv'])->methode
        );
        assertSame(
            Valuation::METHODE_JAEHRLICH,
            Valuation::ausWerten(['assets_depreciation_method' => 'yearly'])->methode
        );
    });

    // ----------------------------------------------------------------
    // Fehlende Grundlagen
    // ----------------------------------------------------------------

    $alle = new Valuation(true, true, true);

    test('Ohne Kaufdatum bleibt alles leer', function () use ($alle) {
        $w = $alle->berechnen(null, 1200.0, 36, '2026-09-17');
        assertSame(null, $w['ende']);
        assertSame(null, $w['restwert']);
        assertSame(null, $w['abgeschrieben']);
        // Der eingegebene Betrag wird trotzdem zurückgegeben.
        assertSame(1200.0, $w['anschaffungswert']);
    });

    test('Ohne Nutzungsdauer bleibt alles leer', function () use ($alle) {
        $w = $alle->berechnen('2024-03-15', 1200.0, null, '2026-09-17');
        assertSame(null, $w['ende']);
        assertSame(null, $w['restwert']);
    });

    test('Ohne Betrag entsteht trotzdem eine Frist', function () use ($alle) {
        $w = $alle->berechnen('2024-03-15', null, 36, '2026-09-17');
        assertSame('2027-03-15', $w['ende']);
        assertSame(5, $w['monate_rest']);
        // Ein Restwert ohne Anschaffungswert wäre geraten – er bleibt leer.
        assertSame(null, $w['restwert']);
        assertSame(null, $w['abgeschrieben']);
    });

    test('Eine unsinnige Nutzungsdauer wird abgewiesen', function () use ($alle) {
        assertSame(null, $alle->berechnen('2024-03-15', 1200.0, 0, '2026-09-17')['ende']);
        assertSame(null, $alle->berechnen('2024-03-15', 1200.0, -12, '2026-09-17')['ende']);
        assertSame(null, $alle->berechnen('2024-03-15', 1200.0, Valuation::MAX_MONATE + 1, '2026-09-17')['ende']);
    });

    // ----------------------------------------------------------------
    // Frist
    // ----------------------------------------------------------------

    test('Das Nutzungsende liegt die Nutzungsdauer nach dem Kaufdatum', function () use ($alle) {
        assertSame('2027-03-15', $alle->berechnen('2024-03-15', null, 36, '2024-03-15')['ende']);
        assertSame('2029-03-01', $alle->berechnen('2024-03-01', null, 60, '2024-03-01')['ende']);
    });

    test('Der Monatsletzte bleibt der Monatsletzte', function () use ($alle) {
        // Der 31.01. plus einen Monat ist der 29.02. und nicht der 02.03.
        assertSame('2024-02-29', $alle->berechnen('2024-01-31', null, 1, '2024-01-31')['ende']);
        assertSame('2025-02-28', $alle->berechnen('2024-01-31', null, 13, '2024-01-31')['ende']);
    });

    test('Der Status folgt der Restlaufzeit', function () use ($alle) {
        assertSame('aktiv',      $alle->berechnen('2024-03-15', null, 36, '2025-01-01')['status']);
        assertSame('laeuft_aus', $alle->berechnen('2024-03-15', null, 36, '2026-10-01')['status']);
        assertSame('abgelaufen', $alle->berechnen('2024-03-15', null, 36, '2027-03-15')['status']);
        assertSame('abgelaufen', $alle->berechnen('2024-03-15', null, 36, '2030-01-01')['status']);
    });

    test('Genutzte und restliche Monate ergeben zusammen die Nutzungsdauer', function () use ($alle) {
        foreach (['2024-03-15', '2025-06-30', '2026-09-17', '2027-03-15', '2028-01-01'] as $tag) {
            $w = $alle->berechnen('2024-03-15', null, 36, $tag);
            assertSame(36, $w['monate_genutzt'] + $w['monate_rest'], 'Stichtag ' . $tag);
        }
    });

    test('Ein künftiges Kaufdatum verbraucht noch nichts', function () use ($alle) {
        $w = $alle->berechnen('2027-01-01', 1200.0, 36, '2026-09-17');
        assertSame(0, $w['monate_genutzt']);
        assertSame(0.0, $w['fortschritt']);
        assertSame(0.0, $w['abgeschrieben']);
        assertSame(1200.0, $w['restwert']);
    });

    // ----------------------------------------------------------------
    // Beträge
    // ----------------------------------------------------------------

    test('Die monatsgenaue Abschreibung zählt den Anschaffungsmonat voll', function () use ($alle) {
        // Anschaffung im März 2024, 36 Monate à 33,33 €.
        assertSame(33.33, $alle->berechnen('2024-03-15', 1200.0, 36, '2024-03-15')['abgeschrieben']);
        assertSame(66.67, $alle->berechnen('2024-03-15', 1200.0, 36, '2024-04-01')['abgeschrieben']);
        assertSame(66.67, $alle->berechnen('2024-03-15', 1200.0, 36, '2024-04-30')['abgeschrieben']);
    });

    test('Volle Jahre buchen erst nach dem Jahrestag', function () {
        $v = new Valuation(true, true, true, Valuation::METHODE_JAEHRLICH);
        assertSame(0.0,   $v->berechnen('2024-03-15', 1200.0, 36, '2025-03-14')['abgeschrieben']);
        assertSame(400.0, $v->berechnen('2024-03-15', 1200.0, 36, '2025-03-15')['abgeschrieben']);
        assertSame(800.0, $v->berechnen('2024-03-15', 1200.0, 36, '2026-09-17')['abgeschrieben']);
    });

    test('Restwert und abgeschriebener Anteil ergeben den Anschaffungswert', function () {
        foreach ([Valuation::METHODE_MONATLICH, Valuation::METHODE_JAEHRLICH] as $methode) {
            $v = new Valuation(true, true, true, $methode);
            foreach (['2024-03-15', '2025-01-01', '2026-09-17', '2027-03-15'] as $tag) {
                foreach ([1.0, 1200.0, 99999.99] as $wert) {
                    $w = $v->berechnen('2024-03-15', $wert, 36, $tag);
                    assertTrue(
                        abs(($w['restwert'] + $w['abgeschrieben']) - $wert) < 0.01,
                        "{$methode} / {$tag} / {$wert}"
                    );
                }
            }
        }
    });

    test('Nach Ablauf der Nutzungsdauer steht der Restwert auf null', function () {
        foreach ([Valuation::METHODE_MONATLICH, Valuation::METHODE_JAEHRLICH] as $methode) {
            $v = new Valuation(true, true, true, $methode);
            $w = $v->berechnen('2024-03-15', 1200.0, 36, '2030-01-01');
            assertSame(0.0, $w['restwert'], $methode);
            assertSame(1200.0, $w['abgeschrieben'], $methode);
        }
    });

    test('Der Restwert fällt nie unter null', function () {
        $v = new Valuation(true, true, true);
        foreach (['2027-03-15', '2027-04-01', '2040-01-01'] as $tag) {
            assertTrue($v->berechnen('2024-03-15', 1200.0, 36, $tag)['restwert'] >= 0.0, $tag);
        }
    });

    test('Jahres- und Monatsbetrag passen zur Nutzungsdauer', function () use ($alle) {
        $w = $alle->berechnen('2024-03-15', 1200.0, 36, '2024-03-15');
        assertSame(33.33, $w['monatsbetrag']);
        assertSame(400.0, $w['jahresbetrag']);

        $kurz = $alle->berechnen('2024-03-15', 1200.0, 12, '2024-03-15');
        assertSame(100.0, $kurz['monatsbetrag']);
        assertSame(1200.0, $kurz['jahresbetrag']);
    });

    // ----------------------------------------------------------------
    // Eingaben aus dem Formular
    // ----------------------------------------------------------------

    test('Beträge werden mit Komma wie mit Punkt gelesen', function () {
        assertSame(1234.56, Valuation::wertAusEingabe('1234.56'));
        assertSame(1234.56, Valuation::wertAusEingabe('1234,56'));
        assertSame(1234.56, Valuation::wertAusEingabe('1.234,56'));
        assertSame(1234.56, Valuation::wertAusEingabe(' 1 234,56 '));
        assertSame(0.0, Valuation::wertAusEingabe('0'));
    });

    test('Unbrauchbare Beträge ergeben keinen Wert', function () {
        assertSame(null, Valuation::wertAusEingabe(''));
        assertSame(null, Valuation::wertAusEingabe('   '));
        assertSame(null, Valuation::wertAusEingabe('teuer'));
        assertSame(null, Valuation::wertAusEingabe('-5'));
        assertSame(null, Valuation::wertAusEingabe(null));
        assertSame(null, Valuation::wertAusEingabe(['1200']));
        assertSame(null, Valuation::wertAusEingabe('99999999999999'));
    });

    test('Die Nutzungsdauer nimmt nur ganze Monate im erlaubten Bereich', function () {
        assertSame(36, Valuation::monateAusEingabe('36'));
        assertSame(1, Valuation::monateAusEingabe('1'));
        assertSame(Valuation::MAX_MONATE, Valuation::monateAusEingabe((string)Valuation::MAX_MONATE));
        assertSame(null, Valuation::monateAusEingabe('0'));
        assertSame(null, Valuation::monateAusEingabe('-12'));
        assertSame(null, Valuation::monateAusEingabe('36,5'));
        assertSame(null, Valuation::monateAusEingabe(''));
        assertSame(null, Valuation::monateAusEingabe(null));
        assertSame(null, Valuation::monateAusEingabe((string)(Valuation::MAX_MONATE + 1)));
    });

    // ----------------------------------------------------------------
    // Oberfläche
    // ----------------------------------------------------------------

    test('Die Geräteseite zeigt jede Angabe nur bei zugeschaltetem Schalter', function () {
        $quelle = file_get_contents(__DIR__ . '/../templates/assets/show.php');

        foreach (['Anschaffungswert', 'Nutzungsdauer', 'Abschreibung', 'Restwert'] as $beschriftung) {
            // Die Beschriftung kann übersetzt sein – dann steht sie in einem te()-Aufruf.
            assertTrue(
                str_contains($quelle, '<div class="inv-detail-label">' . $beschriftung . '</div>')
                    || str_contains($quelle, '<div class="inv-detail-label"><?= $this->te(\'' . $beschriftung . '\') ?></div>'),
                'Zeile fehlt: ' . $beschriftung
            );
        }

        // Jede der vier Zeilen hängt an einer eigenen Bedingung.
        foreach (['$val->brauchtAnschaffungswert()', '$val->nutzungsdauer', '$val->abschreibung', '$val->restwert'] as $bedingung) {
            assertTrue(str_contains($quelle, 'if ($val && ' . $bedingung . '):'), 'Bedingung fehlt: ' . $bedingung);
        }
    });

    test('Die Einstellungsseite bietet für jeden Schalter ein Kästchen', function () {
        $quelle = file_get_contents(__DIR__ . '/../templates/admin/assets/valuation.php');
        foreach (Valuation::SCHALTER as $key) {
            assertTrue(
                str_contains($quelle, 'name="' . $key . '" value="1"'),
                'Kästchen fehlt: ' . $key
            );
        }
        assertTrue(str_contains($quelle, 'name="_csrf"'), 'CSRF-Feld fehlt');
    });

    test('Der Filter nach Nutzungsdauer greift nur bei zugeschalteter Angabe', function () {
        $quelle = file_get_contents(__DIR__ . '/../src/Controllers/AssetController.php');
        assertTrue(
            str_contains($quelle, "if (\$this->valuation()->nutzungsdauer && \$lifecycle !== '')"),
            'Der Filter ist nicht an den Schalter gebunden'
        );
    });

    test('Ohne Zuschaltung schreibt das Formular keine Werte fort', function () {
        $quelle = file_get_contents(__DIR__ . '/../src/Controllers/AssetController.php');
        // Der Helfer liefert nur zugeschaltete Spalten; beide Schreibwege nutzen ihn.
        assertTrue(str_contains($quelle, 'private function valuationFelder(array $data): array'));
        assertTrue(str_contains($quelle, "\$this->db()->insert('assets', \$this->valuationFelder(\$data) + ["));
        assertTrue(str_contains($quelle, "\$this->db()->update('assets', \$wertFelder + ["));
    });
};