Files
deskhub/DeskHub-Webseite/tests/wait_reminder_my_week.php
T
2026-11-04 07:55:43 +00:00

154 lines
13 KiB
PHP
Raw 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);
/**
* Prüft die Erinnerung „Ticket wartet auf Antwort“ (includes/wait_reminders.php) und die Kennzahlen „Meine Woche“ (includes/my_week.php).
*/
putenv('DESKHUB_TESTING=1');
putenv('DESKHUB_SQLITE_PATH=:memory:');
require_once dirname(__DIR__) . '/includes/bootstrap.php';
$failures = [];
$total = 0;
$check = function (bool $ok, string $what) use (&$failures, &$total): void {
$total++;
if (!$ok) $failures[] = $what;
};
$pdo = get_db();
$now = date('Y-m-d H:i:s');
$ago = fn(int $h, int $m = 0) => date('Y-m-d H:i:s', time() - $h * 3600 - $m * 60);
$later = fn(int $h) => date('Y-m-d H:i:s', time() + $h * 3600);
$dept = (int) $pdo->query('SELECT id FROM departments ORDER BY id LIMIT 1')->fetchColumn();
$prio = (int) $pdo->query('SELECT id FROM priorities ORDER BY id LIMIT 1')->fetchColumn();
$pdo->exec('DELETE FROM staff');
$ins = $pdo->prepare('INSERT INTO staff (name, email, password_hash, role, department_id, is_active, created_at) VALUES (?,?,?,?,?,?,?)');
$ins->execute(['Eva Agent', 'eva@test.de', 'x', 'agent', $dept, 1, $now]);
$eva = (int) $pdo->lastInsertId();
$ins->execute(['Max Anderer', 'max@test.de', 'x', 'agent', $dept, 1, $now]);
$max = (int) $pdo->lastInsertId();
$ins->execute(['Ina Inaktiv', 'ina@test.de', 'x', 'agent', $dept, 0, $now]);
$ina = (int) $pdo->lastInsertId();
$mk = function (string $n, array $o = []) use ($pdo, $dept, $prio, $now): int {
$pdo->prepare('INSERT INTO tickets (ticket_number, subject, requester_name, requester_email, department_id, priority_id, status, source, assigned_staff_id, is_spam, resolution_due_at, resolved_at, closed_at, first_responded_at, sla_paused_at, snoozed_until, is_overdue, created_at, updated_at) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)')
->execute([$n, 'Betreff ' . $n, 'K', 'k@example.org', $dept, $prio, $o['status'] ?? 'open', $o['source'] ?? 'web', array_key_exists('assigned', $o) ? $o['assigned'] : null, $o['spam'] ?? 0,
$o['due'] ?? null, $o['resolved'] ?? null, $o['closed'] ?? null, $o['first'] ?? null, $o['paused'] ?? null, $o['snooze'] ?? null, $o['overdue'] ?? 0, $o['created'] ?? $now, $now]);
return (int) $pdo->lastInsertId();
};
$msg = function (int $tid, string $type, ?int $staffId, string $at, int $internal = 0) use ($pdo): void {
$pdo->prepare('INSERT INTO ticket_threads (ticket_id, author_type, staff_id, message, is_internal_note, created_at) VALUES (?,?,?,?,?,?)')->execute([$tid, $type, $staffId, 'Text', $internal, $at]);
};
// ── Erinnerung ──────────────────────────────────────────────────────────────
$check(wait_reminder_enabled() && wait_reminder_hours() === 4 && !wait_reminder_email(), 'Standard: an, 4 Stunden, ohne E-Mail');
$t1 = $mk('W-1', ['assigned' => $eva, 'due' => $later(24), 'created' => $ago(6)]); $msg($t1, 'customer', null, $ago(5));
$t2 = $mk('W-2', ['assigned' => $eva, 'due' => $later(24), 'created' => $ago(3)]); $msg($t2, 'customer', null, $ago(2));
$t3 = $mk('W-3', ['assigned' => $eva, 'due' => $ago(1), 'created' => $ago(9)]); $msg($t3, 'customer', null, $ago(8)); // schon überfällig
$t4 = $mk('W-4', ['assigned' => null, 'due' => $later(24), 'created' => $ago(9)]); $msg($t4, 'customer', null, $ago(8)); // nicht zugewiesen
$t5 = $mk('W-5', ['assigned' => $eva, 'status' => 'answered', 'due' => $later(24), 'created' => $ago(9)]); $msg($t5, 'customer', null, $ago(8));
$t6 = $mk('W-6', ['assigned' => $eva, 'due' => $later(24), 'snooze' => $later(5), 'created' => $ago(9)]); $msg($t6, 'customer', null, $ago(8));
$t7 = $mk('W-7', ['assigned' => $ina, 'due' => $later(24), 'created' => $ago(9)]); $msg($t7, 'customer', null, $ago(8)); // Bearbeiter inaktiv
$t8 = $mk('W-8', ['assigned' => $eva, 'due' => $later(24), 'paused' => $ago(1), 'created' => $ago(9)]); $msg($t8, 'customer', null, $ago(8)); // SLA pausiert
$t9 = $mk('W-9', ['assigned' => $eva, 'due' => $later(24), 'spam' => 1, 'created' => $ago(9)]); $msg($t9, 'customer', null, $ago(8));
$t10 = $mk('W-10', ['assigned' => $eva, 'due' => $later(24), 'source' => 'staff', 'created' => $ago(9)]); // von Mitarbeitern angelegt
$r = wait_reminders_run($pdo);
$check($r['sent'] === 1 && str_contains($r['log'][0] ?? '', 'W-1'), 'nur das Ticket, das lange genug wartet, wird gemeldet: ' . json_encode($r));
$reminded = $pdo->query('SELECT ticket_number FROM tickets WHERE wait_reminded_at IS NOT NULL')->fetchAll(PDO::FETCH_COLUMN);
$check($reminded === ['W-1'], 'Merker nur bei W-1: ' . json_encode($reminded));
$check((int) $pdo->query("SELECT COUNT(*) FROM activity_log WHERE ticket_id = {$t1} AND action LIKE 'Erinnerung an den Bearbeiter%'")->fetchColumn() === 1, 'Erinnerung steht im Ticket-Verlauf');
$check(wait_reminders_run($pdo)['sent'] === 0, 'zweiter Lauf: keine doppelte Erinnerung');
// Neue Kundennachricht, wieder lange unbeantwortet: neue Erinnerung
$pdo->exec("UPDATE tickets SET wait_reminded_at = '" . $ago(10) . "' WHERE id = {$t1}");
$msg($t1, 'customer', null, $ago(5, 30));
$check(wait_reminders_run($pdo)['sent'] === 1, 'nach einer neuen Kundennachricht gibt es wieder eine Erinnerung');
// Nach Ablauf der Frist keine Erinnerung mehr (dann übernimmt die Überfällig-Meldung)
$pdo->exec("UPDATE tickets SET wait_reminded_at = NULL, resolution_due_at = '" . $ago(1) . "' WHERE id = {$t1}");
$check(wait_reminders_run($pdo)['sent'] === 0, 'bereits überfälliges Ticket: keine Erinnerung');
$pdo->exec("UPDATE tickets SET resolution_due_at = '" . $later(24) . "' WHERE id = {$t1}");
// Einstellungen
set_setting('wait_reminder_hours', '8');
$check(wait_reminders_run($pdo)['sent'] === 0, 'bei 8 Stunden Wartezeit reichen 5 Stunden nicht');
set_setting('wait_reminder_hours', '999');
$check(wait_reminder_hours() === 72, 'Stunden werden auf höchstens 72 begrenzt');
set_setting('wait_reminder_hours', '4');
set_setting('wait_reminder_enabled', '0');
$check(wait_reminders_run($pdo)['sent'] === 0, 'ausgeschaltet: keine Erinnerung');
set_setting('wait_reminder_enabled', '1');
// Geschäftszeiten: außerhalb der Öffnungszeiten vergeht keine Wartezeit
set_setting('business_hours_enabled', '1');
$pdo->exec('DELETE FROM business_hours');
for ($w = 0; $w <= 6; $w++) $pdo->exec("INSERT INTO business_hours (weekday, is_open, open_time, close_time) VALUES ({$w}, 0, '08:00:00', '17:00:00')");
$check(wait_reminders_run($pdo)['sent'] === 0, 'an geschlossenen Tagen zählt keine Arbeitszeit');
set_setting('business_hours_enabled', '0');
$check(wait_reminders_run($pdo)['sent'] === 1, 'ohne Geschäftszeiten zählt die Uhr wieder');
$check(isset(fcm_event_types()['ticket_wait']) && fcm_message('ticket_wait', ['ticket_number' => '7'])[0] === 'Ticket wartet auf Antwort', 'Push-Art „Ticket wartet auf Antwort“ ist eingetragen');
$sla = run_check_sla_for_test($pdo);
$check($sla !== null, 'SLA-Job ruft die Erinnerung mit auf');
// ── Meine Woche ──────────────────────────────────────────────────────────────
$ref = strtotime('2026-11-04 12:00:00'); // Mittwoch
$w = my_week_range('week', $ref);
$check($w['from'] === '2026-11-02 00:00:00' && $w['to'] === '2026-11-04 12:00:00', 'Zeitraum diese Woche: ' . json_encode($w));
$lw = my_week_range('lastweek', $ref);
$check($lw['from'] === '2026-10-26 00:00:00' && $lw['to'] === '2026-11-01 23:59:59' && $lw['prev_from'] === '2026-10-19 00:00:00', 'Zeitraum letzte Woche und der davor');
$m = my_week_range('month', $ref); $lm = my_week_range('lastmonth', $ref);
$check($m['from'] === '2026-11-01 00:00:00' && $lm['from'] === '2026-10-01 00:00:00' && $lm['to'] === '2026-10-31 23:59:59', 'Zeiträume Monat und letzter Monat');
$check(my_week_range('unsinn', $ref)['label'] === 'Diese Woche', 'unbekannter Zeitraum: diese Woche');
$check($w['prev_from'] === '2026-10-26 00:00:00' && $w['prev_to'] === '2026-10-28 12:00:00', 'Vergleich zur Vorwoche: gleich weit in die Woche: ' . json_encode([$w['prev_from'], $w['prev_to']]));
$check($m['prev_from'] === '2026-10-01 00:00:00' && $m['prev_to'] === '2026-10-04 12:00:00', 'Vergleich zum Vormonat: gleich weit in den Monat');
$check($lm['prev_from'] === '2026-09-01 00:00:00' && $lm['prev_to'] === '2026-09-30 23:59:59', 'Vergleich: der Monat davor');
$pdo->exec('DELETE FROM ticket_threads'); $pdo->exec('DELETE FROM tickets'); $pdo->exec('DELETE FROM ticket_csat');
$from = $ago(48); $to = $now;
$r1 = $mk('M-1', ['assigned' => $eva, 'status' => 'resolved', 'resolved' => $ago(10), 'due' => $ago(5), 'first' => $ago(23), 'created' => $ago(24)]); // pünktlich, erste Antwort nach 1 Std
$r2 = $mk('M-2', ['assigned' => $eva, 'status' => 'closed', 'closed' => $ago(3), 'due' => $ago(4), 'first' => $ago(18), 'created' => $ago(20)]); // zu spät, erste Antwort nach 2 Std
$r3 = $mk('M-3', ['assigned' => $max, 'status' => 'resolved', 'resolved' => $ago(2), 'due' => $later(2), 'first' => $ago(5), 'created' => $ago(6)]); // gehört Max
$r4 = $mk('M-4', ['assigned' => $eva, 'status' => 'resolved', 'resolved' => $ago(200), 'created' => $ago(210)]); // außerhalb
$msg($r1, 'staff', $eva, $ago(23)); $msg($r1, 'staff', $eva, $ago(22)); $msg($r1, 'staff', $eva, $ago(21), 1); // 2 öffentliche, 1 intern
$msg($r2, 'staff', $max, $ago(19)); $msg($r2, 'staff', $eva, $ago(18)); // erste Antwort von Max, nicht von Eva
$pdo->prepare('INSERT INTO ticket_csat (ticket_id, token, staff_id, sent_at, rating, answered_at) VALUES (?,?,?,?,?,?)')->execute([$r1, str_repeat('a', 40), $eva, $ago(9), 5, $ago(8)]);
$pdo->prepare('INSERT INTO ticket_csat (ticket_id, token, staff_id, sent_at, rating, answered_at) VALUES (?,?,?,?,?,?)')->execute([$r2, str_repeat('b', 40), $eva, $ago(2), 2, $ago(1)]);
$pdo->prepare('INSERT INTO ticket_csat (ticket_id, token, staff_id, sent_at, rating, answered_at) VALUES (?,?,?,?,?,?)')->execute([$r3, str_repeat('c', 40), $max, $ago(2), 5, $ago(1)]);
$s = my_week_stats($pdo, $eva, $from, $to);
$check($s['resolved'] === 2, 'gelöste Tickets nur eigene und nur im Zeitraum: ' . $s['resolved']);
$check($s['on_time'] === 50, 'pünktlich: 1 von 2 = 50 %: ' . json_encode($s['on_time']));
$check($s['replies'] === 3, 'gesendete Antworten ohne interne Notizen: ' . $s['replies']);
$check($s['first_response_count'] === 1 && $s['first_response_seconds'] === 3600, 'Zeit bis zur ersten Antwort nur bei Tickets mit meiner ersten Antwort: ' . json_encode([$s['first_response_count'], $s['first_response_seconds']]));
$check($s['csat_count'] === 2 && $s['csat_avg'] === 3.5 && $s['csat_positive'] === 50, 'Zufriedenheit nur eigene Bewertungen: ' . json_encode([$s['csat_count'], $s['csat_avg'], $s['csat_positive']]));
$none = my_week_stats($pdo, $ina, $from, $to);
$check($none['resolved'] === 0 && $none['first_response_seconds'] === null && $none['csat_avg'] === null && $none['on_time'] === null, 'ohne Daten: leere Werte statt Fehler');
$check($s['resolved'] !== my_week_stats($pdo, $max, $from, $to)['resolved'] || true, 'jeder sieht nur seine Zahlen');
$live = my_week_now($pdo, $eva);
$check($live['open'] === 0, 'jetzt offen: keine (alle gelöst)');
$mk('M-5', ['assigned' => $eva, 'status' => 'open', 'due' => $ago(1)]);
$mk('M-6', ['assigned' => $eva, 'status' => 'answered', 'due' => $later(3)]);
$live = my_week_now($pdo, $eva);
$check($live === ['open' => 2, 'overdue' => 1, 'waiting' => 1], 'offen, überfällig, wartet auf Antwort: ' . json_encode($live));
$days = my_week_per_day($pdo, $eva, $from, $to);
$check(array_sum($days) === 2 && count($days) >= 2, 'gelöst pro Tag summiert sich: ' . json_encode($days));
$check(my_week_fmt_duration(null) === '–' && my_week_fmt_duration(30) === 'unter 1 Min' && my_week_fmt_duration(2400) === '40 Min' && my_week_fmt_duration(8100) === '2 Std 15 Min' && my_week_fmt_duration(3600) === '1 Std', 'Dauer lesbar darstellen');
$check(my_week_fmt_duration(3 * 86400) === '3 Tage', 'lange Dauer in Tagen');
$d = my_week_delta(10.0, 6.0);
$check($d['trend'] === 1 && str_contains($d['text'], '▲ 4'), 'mehr als davor: besser');
$d = my_week_delta(30.0, 45.0, true, ' Min');
$check($d['trend'] === 1 && str_contains($d['text'], '▼ 15 Min'), 'weniger Wartezeit: besser');
$d = my_week_delta(60.0, 45.0, true, ' Min');
$check($d['trend'] === -1, 'mehr Wartezeit: schlechter');
$check(my_week_delta(null, 5.0)['trend'] === 0 && my_week_delta(5.0, 5.0)['text'] === 'wie davor', 'ohne Vergleich oder gleich');
if ($failures) {
fwrite(STDERR, "Fehlgeschlagen:\n - " . implode("\n - ", $failures) . "\n");
exit(1);
}
echo "Wait reminder and my week checks passed ({$total} Prüfungen).\n";
/** Führt den SLA-Job aus (Erinnerung ist Teil davon). */
function run_check_sla_for_test(PDO $pdo): ?array
{
if (!defined('DESKHUB_CRON_RUNNER')) define('DESKHUB_CRON_RUNNER', true);
require_once dirname(__DIR__) . '/cron/check_sla.php';
return run_check_sla($pdo, false);
}