prepare('INSERT INTO tickets (ticket_number, subject, requester_name, requester_email, department_id, priority_id, status, source, is_spam, closed_at, resolved_at, created_at, updated_at) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?)') ->execute(["CS-{$n}", "Betreff {$n}", 'Erika Muster', $email, 1, 2, $status, $source, $spam, $status === 'closed' ? $closedAt : null, $status === 'resolved' ? $closedAt : null, $now, $now]); return (int) $pdo->lastInsertId(); }; $old = date('Y-m-d H:i:s', time() - 3600); $t1 = $mk('closed', 'erika@kunde.example', $old); $check(csat_send_due($pdo) === 0, 'ausgeschaltet: nichts wird gesendet'); csat_set_enabled(true, 30); $check(csat_enabled() && get_setting('csat_since', '') !== '', 'Einschalten merkt sich den Zeitpunkt'); $check(csat_send_due($pdo) === 0, 'Ticket aus der Zeit vor dem Einschalten bekommt keine Abfrage'); set_setting('csat_since', date('Y-m-d H:i:s', time() - 86400)); $tNew = $mk('closed', 'neu@kunde.example', date('Y-m-d H:i:s', time() - 60)); $tSpam = $mk('closed', 'spam@kunde.example', $old, 'web', 1); $tChat = $mk('closed', 'chat@kunde.example', $old, 'chat'); $tOpen = $mk('open', 'offen@kunde.example', null); $tNoMail = $mk('closed', '', $old); $tBad = $mk('closed', 'kein-email', $old); $tRes = $mk('resolved', 'geloest@kunde.example', $old); $sent = csat_send_due($pdo); $rows = $pdo->query('SELECT ticket_id FROM ticket_csat ORDER BY ticket_id')->fetchAll(PDO::FETCH_COLUMN); $check($sent === 2 && array_map('intval', $rows) === [$t1, $tRes], 'gesendet nur für geschlossene/gelöste Tickets mit Adresse, nach der Wartezeit, ohne Spam und Chat: ' . json_encode($rows)); $check(csat_send_due($pdo) === 0, 'pro Ticket nur eine Abfrage'); $pdo->prepare('UPDATE tickets SET updated_at = ?, closed_at = ? WHERE id = ?')->execute([$old, $old, $tNew]); $check(csat_send_due($pdo) === 1, 'nach Ablauf der Wartezeit kommt auch das neuere Ticket dran'); $row = csat_for_ticket($pdo, $t1); $token = (string) $row['token']; $check(preg_match('/^[a-f0-9]{40}$/', $token) === 1 && csat_find_by_token($pdo, $token)['ticket_number'] === 'CS-5001', 'Token und Suche'); $check(csat_find_by_token($pdo, 'zzz') === null && csat_find_by_token($pdo, str_repeat('a', 40)) === null, 'ungültiger Token'); $check(str_contains(csat_mail_html($pdo->query("SELECT * FROM tickets WHERE id = {$t1}")->fetch(), $token), 'csat.php?t=' . $token . '&r=5') || str_contains(csat_mail_html($pdo->query("SELECT * FROM tickets WHERE id = {$t1}")->fetch(), $token), 'r=5'), 'Mail enthält die fünf Sterne-Links'); $check(!csat_record($pdo, $token, 0, '')['ok'] && !csat_record($pdo, $token, 6, '')['ok'], 'Bewertung muss 1 bis 5 sein'); $check(csat_record($pdo, $token, 4, ' Schnell und freundlich ')['ok'], 'Bewertung wird gespeichert'); $check(!csat_record($pdo, $token, 1, '')['ok'], 'zweite Bewertung wird abgelehnt'); $row = csat_for_ticket($pdo, $t1); $check((int) $row['rating'] === 4 && $row['comment'] === 'Schnell und freundlich' && $row['answered_at'] !== null, 'Bewertung und Kommentar stehen in der Datenbank'); $check(!csat_record($pdo, 'x', 3, '')['ok'], 'unbekannter Link'); $tok2 = (string) csat_for_ticket($pdo, $tRes)['token']; csat_record($pdo, $tok2, 2, ''); $st = csat_stats($pdo, date('Y-m-d 00:00:00', time() - 86400), date('Y-m-d 23:59:59'), null); $check($st['sent'] === 3 && $st['answered'] === 2 && $st['avg'] === 3.0 && $st['rate'] === 66.7 && $st['dist'][4] === 1 && $st['dist'][2] === 1 && count($st['recent']) === 2, 'Auswertung: ' . json_encode($st)); $check(csat_stats($pdo, '2000-01-01 00:00:00', '2000-12-31 23:59:59', null)['sent'] === 0 && csat_stats($pdo, $now, $now, [])['sent'] === 0, 'leere Zeiträume und Abteilungen'); $check(csat_stars(4) === '★★★★☆' && csat_stars(9) === '★★★★★', 'Sterne-Text'); csat_set_enabled(false, 30); $check(!csat_enabled(), 'Ausschalten'); // ── Statusseite ───────────────────────────────────────────────────────────── $check(!status_page_enabled() && status_overall($pdo)['key'] === 'operational', 'Statusseite standardmäßig aus, ohne Dienste alles in Ordnung'); $web = status_save_component($pdo, null, 'Webseite', 'Kundenportal', 10); $mail = status_save_component($pdo, null, 'E-Mail', '', 20); $check($web !== null && $mail !== null && status_save_component($pdo, null, ' ', '', 0) === null, 'Dienste anlegen (leerer Name abgelehnt)'); $check(!status_set_component($pdo, $web, 'kaputt') && status_set_component($pdo, $web, 'degraded') && status_overall($pdo)['key'] === 'degraded', 'Zustand setzen, ungültiger Zustand abgelehnt'); status_set_component($pdo, $mail, 'outage'); $check(status_overall($pdo)['key'] === 'outage' && status_overall($pdo)['label'] === 'Ausfall', 'schlechtester Zustand zählt'); status_set_component($pdo, $mail, 'maintenance'); status_set_component($pdo, $web, 'operational'); $check(status_overall($pdo)['key'] === 'maintenance', 'Wartung'); status_set_component($pdo, $mail, 'operational'); $r = status_create_incident($pdo, ' ', 'x', 'investigating', null, null, 1); $check(!$r['ok'], 'Meldung ohne Titel wird abgelehnt'); $r = status_create_incident($pdo, 'E-Mail verzögert', 'Wir untersuchen das.', 'investigating', $mail, 'degraded', 1); $check($r['ok'] && status_overall($pdo)['key'] === 'degraded', 'Meldung setzt den Dienst auf „eingeschränkt“'); $inc = (int) $r['id']; $check(count(status_incidents($pdo, 14, true)) === 1 && status_incidents($pdo)[0]['updates'][0]['body'] === 'Wir untersuchen das.', 'offene Meldung mit erstem Update'); $other = status_create_incident($pdo, 'Zweite Störung', '', 'identified', $mail, 'outage', 1)['id']; $check(status_update_incident($pdo, $inc, 'resolved', 'Behoben.', 1)['ok'] && status_overall($pdo)['key'] === 'outage', 'Dienst bleibt gestört, solange eine andere Meldung offen ist'); $check(status_update_incident($pdo, $other, 'resolved', 'Auch behoben.', 1)['ok'] && status_overall($pdo)['key'] === 'operational', 'nach der letzten Meldung wieder „In Betrieb“'); $check(!status_update_incident($pdo, 999, 'resolved', '', 1)['ok'] && !status_update_incident($pdo, $inc, 'quatsch', '', 1)['ok'], 'unbekannte Meldung / Stufe'); $check(status_incidents($pdo, 14, true) === [] && count(status_incidents($pdo, 14)) === 2 && status_incidents($pdo)[0]['resolved_at'] !== null, 'behobene Meldungen bleiben 14 Tage sichtbar'); $pdo->prepare('UPDATE status_incidents SET created_at = ? WHERE id = ?')->execute([date('Y-m-d H:i:s', time() - 30 * 86400), $inc]); $check(count(status_incidents($pdo, 14)) === 1, 'ältere behobene Meldungen verschwinden'); $sum = status_summary($pdo); $check($sum['enabled'] === false && count($sum['components']) === 2 && $sum['overall']['key'] === 'operational' && count($sum['componentStates']) === 4 && str_ends_with($sum['url'], '/status.php'), 'Zusammenfassung für die Apps'); status_delete_incident($pdo, $inc); status_delete_component($pdo, $mail); $check(count(status_components($pdo)) === 1 && (int) $pdo->query("SELECT COUNT(*) FROM status_incident_updates WHERE incident_id = {$inc}")->fetchColumn() === 0, 'Löschen räumt die Updates mit auf'); // ── Ticket-Vorlagen ───────────────────────────────────────────────────────── $check(!ticket_template_save($pdo, null, ['name' => ' '])['ok'], 'Vorlage ohne Namen wird abgelehnt'); $a = ticket_template_save($pdo, null, ['name' => 'Neuer Mitarbeiter', 'subject' => 'Neuer Mitarbeiter: ', 'body' => "Name:\nStartdatum:", 'department_id' => '1', 'priority_id' => '2', 'is_public' => '1', 'sort_order' => '10']); $b = ticket_template_save($pdo, null, ['name' => 'Interne Vorlage', 'subject' => 'Intern', 'department_id' => '9999', 'is_public' => '', 'sort_order' => '20']); $check($a['ok'] && $b['ok'], 'Vorlagen anlegen'); $check(count(ticket_templates_list($pdo)) === 2 && count(ticket_templates_list($pdo, true)) === 1 && ticket_templates_list($pdo, true)[0]['name'] === 'Neuer Mitarbeiter', 'Kunden sehen nur öffentliche Vorlagen'); $tb = ticket_template_get($pdo, $b['id']); $check($tb['department_id'] === null && ticket_template_get($pdo, $b['id'], true) === null && ticket_template_get($pdo, 9999) === null, 'unbekannte Abteilung wird verworfen, nicht öffentliche Vorlage bleibt Kunden verborgen'); $check(ticket_template_save($pdo, $a['id'], ['name' => 'Neuer Mitarbeiter 2', 'is_public' => '1', 'is_active' => '0'])['ok'] && ticket_template_get($pdo, $a['id']) === null && count(ticket_templates_list($pdo, false, true)) === 2, 'Vorlage deaktivieren'); ticket_template_save($pdo, $a['id'], ['name' => 'Neuer Mitarbeiter', 'subject' => 'Neuer Mitarbeiter: ', 'body' => "Name:\nStartdatum:", 'department_id' => '1', 'is_public' => '1', 'is_active' => '1']); $p = ticket_template_payload(ticket_template_get($pdo, $a['id'])); $check($p['name'] === 'Neuer Mitarbeiter' && $p['departmentId'] === 1 && $p['priorityId'] === null && $p['isPublic'] === true && $p['body'] === "Name:\nStartdatum:", 'Vorlage als Feldliste'); ticket_template_delete($pdo, $b['id']); $check(count(ticket_templates_list($pdo)) === 1, 'Vorlage löschen'); // ── Seiten ────────────────────────────────────────────────────────────────── $_SERVER['REQUEST_METHOD'] = 'GET'; $render = function (string $file, array $get = [], array $post = [], string $method = 'GET') { $_GET = $get; $_POST = $post; $_SERVER['REQUEST_METHOD'] = $method; ob_start(); try { include dirname(__DIR__) . '/' . $file; } finally { $html = (string) ob_get_clean(); } return $html; }; // Bewertungsseite csat_set_enabled(true, 30); $pdo->prepare('UPDATE ticket_csat SET rating = NULL, comment = NULL, answered_at = NULL WHERE ticket_id = ?')->execute([$t1]); $html = $render('csat.php', ['t' => $token, 'r' => '5']); $check(str_contains($html, 'Wie zufrieden waren Sie?') && str_contains($html, 'CS-5001') && str_contains($html, 'value="5" style="position:absolute;opacity:0;width:0;height:0" checked'), 'Bewertungsseite zeigt das Ticket mit vorgewähltem Stern'); $check((int) csat_for_ticket($pdo, $t1)['answered_at'] === 0 && csat_for_ticket($pdo, $t1)['answered_at'] === null, 'Aufrufen des Links bewertet noch nichts'); $html = $render('csat.php', [], ['t' => $token, 'rating' => '3', 'comment' => 'ok'], 'POST'); $check(str_contains($html, 'Vielen Dank') && (int) csat_for_ticket($pdo, $t1)['rating'] === 3, 'Absenden speichert die Bewertung'); $check(str_contains($render('csat.php', ['t' => $token]), 'Bereits bewertet'), 'zweiter Aufruf: bereits bewertet'); $check(str_contains($render('csat.php', ['t' => 'unsinn']), 'Link nicht gültig'), 'ungültiger Link'); // Statusseite set_setting('status_page_enabled', '1'); status_set_component($pdo, $web, 'degraded'); status_create_incident($pdo, 'Portal langsam', 'Wir arbeiten daran.', 'identified', $web, null, 1); $html = $render('status.php'); $check(str_contains($html, 'Systemstatus') && str_contains($html, 'Teilweise eingeschränkt') && str_contains($html, 'Webseite') && str_contains($html, 'Portal langsam') && str_contains($html, 'Wir arbeiten daran.') && str_contains($html, 'Ursache gefunden'), 'Statusseite zeigt Dienste und Meldung'); $check(str_contains($html, 'href="/status.php"'), 'Link „Status“ im Kundenbereich'); // Ticket-Formular mit Vorlage $tpl = (int) $a['id']; $html = $render('ticket_new.php', ['template' => (string) $tpl]); $check(str_contains($html, 'value="Neuer Mitarbeiter:"') && str_contains($html, "Name:\nStartdatum:") && str_contains($html, 'ticket_new.php?template=' . $tpl) && str_contains($html, 'Ohne Vorlage'), 'Kundenformular wird von der Vorlage vorbelegt'); $html = $render('ticket_new.php'); $check(!str_contains($html, 'value="Neuer Mitarbeiter:"') && str_contains($html, 'Womit können wir helfen'), 'ohne Vorlage bleibt das Formular leer, Vorlagen werden angeboten'); $html = $render('ticket_new.php', ['template' => '99999']); $check(!str_contains($html, 'Ohne Vorlage'), 'unbekannte Vorlage wird ignoriert'); if ($failures) { fwrite(STDERR, "Fehlgeschlagen:\n - " . implode("\n - ", $failures) . "\n"); exit(1); } echo 'CSAT, Status und Vorlagen checks passed.' . "\n";