154 lines
13 KiB
PHP
154 lines
13 KiB
PHP
<?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);
|
||
}
|