OpenNIT

Quelltext des aktuellen Standes

Fassung 1.1.1

LoanHandoverTest.php

11 KB
<?php

declare(strict_types=1);

use OpenNIT\Audit\AuditLogger;
use OpenNIT\Content\LoanManager;
use OpenNIT\Core\Database;

/**
 * Deckt die Unterscheidung zwischen Reservierung und Ausgabe ab.
 *
 * Eine Ausgabe hält fest, dass ein Gerät den Besitzer gewechselt hat, und
 * benennt die ausgebende Person. Wer aus der Ferne für eine Kollegin bucht,
 * gibt nichts aus – deshalb entsteht eine Ausgabe nur auf ausdrücklichen
 * Wunsch, und ein Ausgabedatum in der Zukunft nie.
 */
return static function (): void {

    // ----------------------------------------------------------------
    // Die Entscheidung selbst
    // ----------------------------------------------------------------

    test('Ohne ausdrücklichen Wunsch entsteht keine Ausgabe', function () {
        // Kern der Unterscheidung: Das Datum allein begründet keine Ausgabe.
        foreach (['2026-09-03', '2026-09-01', '2026-12-24'] as $start) {
            assertFalse(
                LoanManager::issuesImmediately(false, $start, '2026-09-03'),
                'Ausgabe ohne Wunsch bei ' . $start
            );
        }
    });

    test('Mit Wunsch entsteht heute und rückwirkend eine Ausgabe', function () {
        // Heute am Ausgabetisch, und die Nacherfassung eines vergangenen Tages.
        assertTrue(LoanManager::issuesImmediately(true, '2026-09-03', '2026-09-03'));
        assertTrue(LoanManager::issuesImmediately(true, '2026-08-20', '2026-09-03'));
    });

    test('Ein künftiges Ausgabedatum lässt sich nie sofort ausgeben', function () {
        // Auch ein manipulierter Formularwert führt nur zur Reservierung.
        assertFalse(LoanManager::issuesImmediately(true, '2026-09-04', '2026-09-03'));
        assertFalse(LoanManager::issuesImmediately(true, '2027-01-01', '2026-09-03'));
    });

    test('Der Vergleichstag ist der heutige, wenn keiner genannt wird', function () {
        $heute  = date('Y-m-d');
        $morgen = date('Y-m-d', strtotime('+1 day'));

        assertTrue(LoanManager::issuesImmediately(true, $heute));
        assertFalse(LoanManager::issuesImmediately(true, $morgen));
        assertFalse(LoanManager::issuesImmediately(false, $heute));
    });

    // ----------------------------------------------------------------
    // Was in der Ausleihe landet
    // ----------------------------------------------------------------

    /**
     * Legt einen Vorgang an und liefert die geschriebene Zeile.
     *
     * @return array{row:array<string,mixed>,audit:array<int,string>}
     */
    $anlegen = static function (string $start, bool $sofort): array {
        $db = new class extends Database {
            /** @var array<string,mixed> */
            public array $lendingRow = [];
            /** @var array<int,array{table:string,data:array}> */
            public array $inserts = [];

            public function __construct()
            {
                parent::__construct([]);
            }

            public function fetchOne(string $sql, array $params = []): ?array
            {
                // Gerät für die Sperre und für refreshDeviceStatus()
                return ['id' => 5, 'title' => 'Leih-Notebook', 'status' => 'available'];
            }

            public function fetchColumn(string $sql, array $params = [], int $column = 0): mixed
            {
                // Keine Konflikte, keine konkurrierenden Vorgänge
                return 0;
            }

            public function fetchAll(string $sql, array $params = []): array
            {
                return [];
            }

            public function insert(string $table, array $data): int
            {
                $this->inserts[] = ['table' => $table, 'data' => $data];
                if ($table === 'loan_lendings') {
                    $this->lendingRow = $data;
                }
                return 42;
            }

            public function update(string $table, array $data, string $where = '', array $params = []): int
            {
                return 1;
            }
        };

        $audit = new class($db) extends AuditLogger {
            /** @var array<int,string> */
            public array $aktionen = [];

            public function logWithUser(
                int $userId,
                string $actionCategory,
                string $actionType,
                ?string $objectType = null,
                ?int $objectId = null,
                ?string $objectName = null,
                ?array $details = null,
                string $result = 'success',
                string $riskLevel = 'low',
                ?array $oldValue = null,
                ?array $newValue = null
            ): int {
                $this->aktionen[] = $actionType;
                return 1;
            }
        };

        $manager = new LoanManager($db, $audit);
        $manager->createLending([
            'device_id'     => 5,
            'borrower_name' => 'Kollegin',
            'planned_start' => $start,
            'planned_end'   => $start,
            'issue_now'     => $sofort,
        ], [], 7);

        return ['row' => $db->lendingRow, 'audit' => $audit->aktionen];
    };

    test('Eine Reservierung für heute nennt niemanden als ausgebende Person', function () use ($anlegen) {
        // Der gemeldete Fall: aus dem Homeoffice für eine Kollegin gebucht.
        $e = $anlegen(date('Y-m-d'), false);

        assertSame('reserved', $e['row']['status']);
        assertSame(null, $e['row']['issued_by'], 'Die Reservierung wird jemandem als Ausgabe zugeschrieben');
        assertSame(null, $e['row']['actual_start'], 'Die Reservierung trägt einen Ausgabezeitpunkt');
        assertSame(['reserve_loan_device'], $e['audit']);
    });

    test('Eine Sofortausgabe hält Person und Zeitpunkt fest', function () use ($anlegen) {
        $e = $anlegen(date('Y-m-d'), true);

        assertSame('active', $e['row']['status']);
        assertSame(7, $e['row']['issued_by']);
        assertTrue(is_string($e['row']['actual_start']) && $e['row']['actual_start'] !== '');
        assertSame(['issue_loan_device'], $e['audit']);
    });

    test('Ein künftiger Termin bleibt auch mit Wunsch eine Reservierung', function () use ($anlegen) {
        $e = $anlegen(date('Y-m-d', strtotime('+3 days')), true);

        assertSame('reserved', $e['row']['status']);
        assertSame(null, $e['row']['issued_by']);
        assertSame(['reserve_loan_device'], $e['audit']);
    });

    // ----------------------------------------------------------------
    // Zeitpunkt der Ausgabe
    // ----------------------------------------------------------------

    test('Eine Ausgabe heute trägt die aktuelle Uhrzeit', function () {
        $heute = date('Y-m-d');
        $zeitpunkt = LoanManager::issuedAtFor($heute, $heute);

        assertSame($heute, substr($zeitpunkt, 0, 10));
        assertFalse(str_ends_with($zeitpunkt, '00:00:00') && date('H:i:s') !== '00:00:00');
    });

    test('Eine Nacherfassung trägt den erfassten Tag, nicht den Eingabezeitpunkt', function () {
        // Sonst wäre die Leihdauer um die Zeit bis zur Erfassung zu kurz.
        assertSame('2026-09-01 00:00:00', LoanManager::issuedAtFor('2026-09-01', '2026-09-05'));
        assertSame('2025-12-24 00:00:00', LoanManager::issuedAtFor('2025-12-24', '2026-09-05'));
    });

    // ----------------------------------------------------------------
    // Sichtbarkeit nicht abgeholter Reservierungen
    // ----------------------------------------------------------------

    /** Liefert die WHERE-Klausel der Ausleih-Liste. */
    $klausel = static function (bool $ueberfaellig, bool $nichtAbgeholt, ?string $suche = null): string {
        $k = new ReflectionClass(LoanManager::class);
        $m = $k->getMethod('lendingFilterClause');
        $m->setAccessible(true);
        return $m->invokeArgs($k->newInstanceWithoutConstructor(), [null, $suche, $ueberfaellig, $nichtAbgeholt])[0];
    };

    test('Nicht abgeholt meint eine Reservierung mit verstrichenem Ausgabetag', function () use ($klausel) {
        $k = $klausel(false, true);

        assertTrue(str_contains($k, "l.status = 'reserved'"), 'Der Status wird nicht eingegrenzt');
        assertTrue(str_contains($k, 'l.planned_start < CURDATE()'), 'Der Ausgabetag wird nicht geprüft');
    });

    test('Beide Merkmale zusammen schließen einander nicht aus', function () use ($klausel) {
        // Überfällig setzt „ausgegeben" voraus, nicht abgeholt „reserviert".
        // Mit UND verknüpft bliebe die Liste zwangsläufig leer.
        $k = $klausel(true, true);

        assertTrue(str_contains($k, ' OR '), 'Die Merkmale sind nicht mit ODER verknüpft');
        assertFalse(
            str_contains($k, "l.status = 'active' AND l.planned_end < CURDATE()) AND (l.status = 'reserved'"),
            'Die Merkmale sind mit UND verknüpft und ergeben nie ein Ergebnis'
        );
    });

    test('Die Merkmale bleiben von der Suche getrennt', function () use ($klausel) {
        // Ohne Klammern um die ODER-Gruppe würde die Suche eines der Merkmale
        // aushebeln.
        $k = $klausel(true, true, 'Meier');

        assertTrue(str_contains($k, ') AND (l.borrower_name LIKE ?'), 'Die Gruppen sind nicht geklammert');
    });

    test('Ohne Häkchen entsteht keine Einschränkung', function () use ($klausel) {
        assertSame('', $klausel(false, false));
    });

    test('Ohne Angabe entsteht eine Reservierung', function () use ($anlegen) {
        // Aufrufer, die `issue_now` nicht setzen (etwa die Reservierung aus
        // einem Ticket), reservieren – sie geben nichts aus.
        $db = new class extends Database {
            public array $lendingRow = [];
            public function __construct() { parent::__construct([]); }
            public function fetchOne(string $sql, array $params = []): ?array
            {
                return ['id' => 5, 'title' => 'Leih-Notebook', 'status' => 'available'];
            }
            public function fetchColumn(string $sql, array $params = [], int $column = 0): mixed { return 0; }
            public function fetchAll(string $sql, array $params = []): array { return []; }
            public function insert(string $table, array $data): int
            {
                if ($table === 'loan_lendings') { $this->lendingRow = $data; }
                return 42;
            }
            public function update(string $table, array $data, string $where = '', array $params = []): int { return 1; }
        };
        $audit = new class($db) extends AuditLogger {
            public function logWithUser(
                int $userId, string $actionCategory, string $actionType,
                ?string $objectType = null, ?int $objectId = null, ?string $objectName = null,
                ?array $details = null, string $result = 'success', string $riskLevel = 'low',
                ?array $oldValue = null, ?array $newValue = null
            ): int { return 1; }
        };

        (new LoanManager($db, $audit))->createLending([
            'device_id'     => 5,
            'borrower_name' => 'Kollegin',
            'planned_start' => date('Y-m-d'),
            'planned_end'   => date('Y-m-d'),
        ], [], 7);

        assertSame('reserved', $db->lendingRow['status']);
        assertSame(null, $db->lendingRow['issued_by']);
    });
};