Files
JobcardSystem/tests/ReportWorkflowTest.php

86 lines
6.1 KiB
PHP

<?php
declare(strict_types=1);
require_once __DIR__ . '/../app/Domain/Reporting/ReportFilters.php';
require_once __DIR__ . '/../app/Domain/Reporting/ReportDataMapper.php';
require_once __DIR__ . '/../app/Domain/Reporting/ClientJobcardReport.php';
require_once __DIR__ . '/../app/Domain/Reporting/ClientHistoryReport.php';
require_once __DIR__ . '/../app/Domain/Reporting/TechnicianActivityReport.php';
require_once __DIR__ . '/../app/Domain/Reporting/SlaReport.php';
require_once __DIR__ . '/../app/Domain/Reporting/PrintReportRenderer.php';
$filters = ReportFilters::fromArray([
'client_id' => 7,
'date_from' => '2026-09-01',
'date_to' => '2026-09-30',
'technician_id' => 4,
'status' => 'open',
'priority' => 'high',
'sla' => 'at_risk',
]);
if (!$filters->matches(['client_id' => 7, 'created_at' => '2026-09-10', 'technician_id' => 4, 'status' => 'open', 'priority' => 'high', 'sla_status' => 'at_risk'])) {
throw new RuntimeException('Expected report filters to match all selected criteria.');
}
if ($filters->matches(['client_id' => 7, 'created_at' => '2026-10-01', 'technician_id' => 4, 'status' => 'open', 'priority' => 'high', 'sla_status' => 'at_risk'])) {
throw new RuntimeException('Expected date range to exclude rows outside the range.');
}
$jobcards = (new ClientJobcardReport($filters))->build([
['id' => 2, 'client_id' => 7, 'client_name' => 'Acme', 'reference_no' => 'JC-2', 'status' => 'open', 'priority' => 'high', 'created_at' => '2026-09-10', 'technician_id' => 4, 'technician_name' => 'Tess', 'sla_status' => 'at_risk', 'internal_notes' => 'secret', 'credentials' => 'omit'],
['id' => 1, 'client_id' => 8, 'client_name' => 'Beta', 'reference_no' => 'JC-1', 'status' => 'closed', 'priority' => 'low', 'created_at' => '2026-09-01', 'internal_notes' => 'secret'],
], 'client');
if ($jobcards !== [['client_id' => 7, 'client_name' => 'Acme', 'reference_no' => 'JC-2', 'status' => 'open', 'priority' => 'high', 'created_at' => '2026-09-10']]) {
throw new RuntimeException('Client jobcard report must filter, sort and allow-list deterministically.');
}
$history = (new ClientHistoryReport())->build([
['jobcard_id' => 3, 'client_id' => 7, 'reference_no' => 'JC-3', 'from_status' => 'new', 'to_status' => 'open', 'changed_at' => '2026-09-03', 'changed_by_name' => 'Tess', 'internal_notes' => 'secret'],
], 'client');
if ($history[0] !== ['client_id' => 7, 'reference_no' => 'JC-3', 'from_status' => 'new', 'to_status' => 'open', 'changed_at' => '2026-09-03']) {
throw new RuntimeException('Client history report must exclude internal actor/details.');
}
$activity = (new TechnicianActivityReport())->build([
['technician_id' => 4, 'technician_name' => 'Tess', 'client_id' => 7, 'client_name' => 'Acme', 'work_date' => '2026-09-02', 'hours' => 1.25, 'counts_toward_sla' => true, 'internal_notes' => 'secret'],
['technician_id' => 4, 'technician_name' => 'Tess', 'client_id' => 7, 'client_name' => 'Acme', 'work_date' => '2026-09-03', 'hours' => 2.75, 'counts_toward_sla' => false],
], 'internal');
if ($activity !== [['technician_id' => 4, 'technician_name' => 'Tess', 'client_id' => 7, 'client_name' => 'Acme', 'hours' => 4.0, 'sla_hours' => 1.25]]) {
throw new RuntimeException('Technician activity must aggregate hours deterministically.');
}
$crossClientActivity = (new TechnicianActivityReport())->build([
['id' => 20, 'technician_id' => 4, 'technician_name' => 'Tess', 'client_id' => 8, 'client_name' => 'Beta', 'work_date' => '2026-09-02', 'hours' => 2, 'counts_toward_sla' => true, 'credentials' => 'secret'],
['id' => 10, 'technician_id' => 4, 'technician_name' => 'Tess', 'client_id' => 7, 'client_name' => 'Acme', 'work_date' => '2026-09-02', 'hours' => 1, 'counts_toward_sla' => true, 'internal_notes' => 'secret'],
], 'internal');
if ($crossClientActivity !== [
['technician_id' => 4, 'technician_name' => 'Tess', 'client_id' => 7, 'client_name' => 'Acme', 'hours' => 1.0, 'sla_hours' => 1.0],
['technician_id' => 4, 'technician_name' => 'Tess', 'client_id' => 8, 'client_name' => 'Beta', 'hours' => 2.0, 'sla_hours' => 2.0],
]) {
throw new RuntimeException('Technician activity must preserve client attribution and sort by technician then client.');
}
$clientActivity = (new TechnicianActivityReport())->build([
['technician_id' => 4, 'technician_name' => 'Tess', 'client_id' => 7, 'client_name' => 'Acme', 'work_date' => '2026-09-02', 'hours' => 1, 'counts_toward_sla' => true, 'password' => 'secret'],
], 'client');
if ($clientActivity !== [['client_id' => 7, 'client_name' => 'Acme', 'hours' => 1.0, 'sla_hours' => 1.0]]) {
throw new RuntimeException('Client technician activity projection must exclude technician and secret fields.');
}
$filteredSla = (new SlaReport(ReportFilters::fromArray(['client_id' => 7, 'date_from' => '2026-09-01', 'date_to' => '2026-09-30'])))->build([
['client_id' => 8, 'client_name' => 'Beta', 'allocated_hours' => 10, 'hours' => [1], 'start_date' => '2026-09-01'],
['client_id' => 7, 'client_name' => 'Acme', 'allocated_hours' => 10, 'hours' => [2], 'start_date' => '2026-09-01', 'credentials' => 'secret'],
], 'client');
if ($filteredSla !== [['client_id' => 7, 'client_name' => 'Acme', 'allocated_hours' => 10.0, 'used_hours' => 2.0, 'remaining_hours' => 8.0, 'usage_percentage' => 20.0, 'status' => 'within_limit']]) {
throw new RuntimeException('SLA report must apply filters and expose a safe audience projection.');
}
$html = (new PrintReportRenderer())->render('Client Jobcards', ['Reference', 'Status'], [['JC-2', 'open']]);
if (!str_contains($html, '@media print') || !str_contains($html, '<th>Reference</th>') || !str_contains($html, 'JC-2') || str_contains($html, '<script>')) {
throw new RuntimeException('Print report renderer must emit escaped, print-friendly HTML.');
}
$escapedHtml = (new PrintReportRenderer())->render('Report <x>', ['Value'], [['<script>alert(1)</script>']]);
if (str_contains($escapedHtml, '<script>alert') || !str_contains($escapedHtml, '&lt;script&gt;')) {
throw new RuntimeException('PDF-ready HTML must escape report content.');
}
printf("Report workflow tests: 5 passed\n");