validate(['category' => ' domain ', 'label' => ' Office DNS ', 'username' => ' admin ', 'notes' => ' managed ']); final_scope_same(true, $technicalResult['valid'], 'Valid technical information should survive the final-scope path.'); final_scope_same('domain', $technicalResult['category'], 'Technical information category should be canonical.'); $contacts = new ContactEditCommand(); $existingContacts = [ ['id' => 10, 'client_id' => 7, 'name' => 'Primary', 'email' => 'primary@example.com', 'is_primary' => true], ['id' => 12, 'client_id' => 7, 'name' => 'Backup', 'email' => 'backup@example.com', 'is_primary' => false], ['id' => 20, 'client_id' => 8, 'name' => 'Other client', 'email' => 'other@example.com', 'is_primary' => true], ]; $edit = $contacts->validateEdit(12, ['client_id' => 7, 'name' => ' Backup 2 ', 'email' => 'BACKUP2@example.com'], $existingContacts); final_scope_same(true, $edit['valid'], 'A valid contact edit should be accepted.'); final_scope_same('Backup 2', $edit['name'], 'Contact edit should normalize the name.'); $primary = $contacts->validatePrimary(12, $existingContacts); final_scope_same([10], $primary['replace_primary_contact_ids'], 'Promoting a contact must identify only same-client primary contacts.'); $deletePrimary = $contacts->validateDelete(10, $existingContacts); final_scope_same(12, $deletePrimary['replacement_primary_contact_id'], 'Deleting a primary contact must select the lowest same-client replacement.'); $deleteOnly = $contacts->validateDelete(20, $existingContacts); final_scope_assert(!$deleteOnly['valid'] && isset($deleteOnly['errors']['delete']), 'The sole contact for a client must not be deletable.'); $checks += 4; // Time correction is immutable-by-default: IDs/ownership stay fixed, voids need reasons. $correction = new TimeEntryCorrectionCommand(); $existingEntry = ['id' => 31, 'jobcard_id' => 44, 'technician_id' => 9, 'work_date' => '2026-09-01', 'hours' => 1.0, 'notes' => 'old', 'counts_toward_sla' => true]; $corrected = $correction->validateCorrection($existingEntry, ['hours' => '2.25', 'notes' => ' corrected ']); final_scope_same(true, $corrected['valid'], 'A valid time correction should be accepted.'); final_scope_same(31, $corrected['id'], 'Time correction must retain the original entry ID.'); final_scope_same(44, $corrected['entry']['jobcard_id'], 'Time correction must not move an entry to another jobcard.'); final_scope_same(2.25, $corrected['entry']['hours'], 'Time correction should use the canonical time-entry calculation.'); $changedOwner = $correction->validateCorrection($existingEntry, ['technician_id' => 10]); final_scope_assert(!$changedOwner['valid'] && isset($changedOwner['errors']['technician_id']), 'Time correction must reject ownership changes.'); $voided = $correction->validateVoid($existingEntry, ['reason' => ' Duplicate entry ']); final_scope_same(true, $voided['valid'], 'A void command with a reason should be accepted.'); final_scope_same('Duplicate entry', $voided['void_reason'], 'Void reasons should be trimmed and retained for audit.'); $missingReason = $correction->validateVoid($existingEntry); final_scope_assert(!$missingReason['valid'] && isset($missingReason['errors']['reason']), 'Voiding without a reason must be rejected.'); $alreadyVoided = $correction->validateCorrection([...$existingEntry, 'voided' => true], ['hours' => 2]); final_scope_assert(!$alreadyVoided['valid'] && isset($alreadyVoided['errors']['voided']), 'Voided entries must not be corrected.'); $checks += 6; // Custom roles and permission assignments are normalized, allow-listed, and protect Administrator. $roles = new RoleRecord(); $role = $roles->validate(['name' => ' Dispatch ', 'description' => ' Dispatch team ']); final_scope_same(true, $role['valid'], 'A valid custom role should be accepted.'); final_scope_assert(!$roles->canDelete(['name' => 'Administrator']) && $roles->canRename(['name' => 'Dispatch'], 'Operations'), 'Administrator safeguards and custom-role actions must coexist.'); $permissions = (new PermissionMatrix())->normalize([' clients.view ', 'clients.view', 'reports.view']); final_scope_same(['clients.view', 'reports.view'], $permissions, 'Role permissions should be canonical and deduplicated.'); $checks += 2; // Notification event -> per-user queue DTO preserves recipient isolation and deduplication. $records = new NotificationRecord(); $event = $records->statusChanged(['jobcard_id' => 44, 'to_status' => 'closed', 'recipients' => ['A@example.com', 'B@example.com']]); final_scope_same('jobcard:44:status:closed', $event['deduplication_key'], 'Status notifications need stable deduplication keys.'); $queueDto = (new NotificationQueue())->mapForUser([...$event, 'title' => 'Closed'], 'b@example.com'); final_scope_same('b@example.com', $queueDto['recipient'], 'Notification queue DTOs must target exactly one normalized user.'); final_scope_same('jobcard_status_changed', $queueDto['type'], 'Queue DTOs must preserve event type.'); $validation = $records->validate(['type' => 'assignment_created', 'recipients' => ['not-an-email'], 'deduplication_key' => 'x']); final_scope_assert(!$validation['valid'] && isset($validation['errors']['recipients']), 'Invalid notification recipients must never enter the queue.'); $checks += 3; // Report audience separation: client projection omits technician/internal fields; internal retains attribution. $rows = [['id' => 1, 'client_id' => 7, 'client_name' => 'Acme', 'reference_no' => 'JC-44', 'status' => 'open', 'priority' => 'high', 'created_at' => '2026-09-01', 'technician_id' => 9, 'technician_name' => 'Tech', 'internal_notes' => 'private', 'credentials' => 'secret']]; $clientReport = (new ClientJobcardReport(ReportFilters::fromArray([])))->build($rows, 'client'); final_scope_assert(!array_key_exists('internal_notes', $clientReport[0]) && !array_key_exists('credentials', $clientReport[0]) && !array_key_exists('technician_id', $clientReport[0]), 'Client reports must exclude internal notes, credentials and technician identifiers.'); $internalActivity = (new TechnicianActivityReport())->build([['technician_id' => 9, 'technician_name' => 'Tech', 'client_id' => 7, 'client_name' => 'Acme', 'hours' => 2, 'counts_toward_sla' => true]], 'internal'); $clientActivity = (new TechnicianActivityReport())->build([['technician_id' => 9, 'technician_name' => 'Tech', 'client_id' => 7, 'client_name' => 'Acme', 'hours' => 2, 'counts_toward_sla' => true]], 'client'); final_scope_assert(array_key_exists('technician_id', $internalActivity[0]) && !array_key_exists('technician_id', $clientActivity[0]), 'Report audience must separate internal technician attribution from client output.'); $checks += 2; // Dynamic boundaries plus route-level contracts for technician scope, CSRF, attachments and credentials. $attachment = (new AttachmentValidator(1000))->validate(['name' => 'proof.pdf', 'mime_type' => 'application/pdf', 'size_bytes' => 100]); final_scope_same(true, $attachment['valid'], 'A valid attachment should pass metadata validation.'); $vault = new CredentialVault(base64_encode(random_bytes(SODIUM_CRYPTO_AEAD_XCHACHA20POLY1305_IETF_KEYBYTES))); $stored = $vault->encryptCredential(['id' => 3, 'category' => 'hosting', 'label' => 'Production', 'username' => 'deploy', 'secret' => 'canary-secret']); final_scope_assert(!array_key_exists('secret', $stored) && isset($stored['secret_ciphertext']) && !str_contains(serialize($stored), 'canary-secret'), 'Credential storage must cross the ciphertext boundary.'); final_scope_assert(preg_match('/function can_access_jobcard\(int \$jobcardId\).*?jobcard_id = :jobcard.*?user_id = :user/s', $bootstrap) === 1, 'Technician jobcard access must be scoped by authenticated user.'); final_scope_assert(preg_match('/SELECT DISTINCT c\.id, c\.name.*?ja\.user_id = :user/s', $frontController) === 1, 'Technician client lists must be scoped by assignment.'); final_scope_assert(substr_count($frontController, 'verify_csrf();') >= 7, 'State-changing routes must use the central CSRF guard.'); final_scope_assert(str_contains($frontController, "if ((\$_SERVER['REQUEST_METHOD'] ?? 'GET') !== 'POST')") && str_contains($frontController, "if (\$route === 'logout')"), 'Logout must be POST-only as well as CSRF-protected.'); final_scope_assert(str_contains($frontController, "header('X-Content-Type-Options: nosniff')") && str_contains($frontController, "basename(\$attachment['stored_name'])"), 'Attachment downloads must use safe names and nosniff.'); final_scope_assert(str_contains($frontController, 'WHERE id = :id AND client_id = :client AND is_active = 1') && str_contains($frontController, "header('Cache-Control: no-store"), 'Credential reveal must bind client ownership and disable caching.'); $checks += 7; // Healthcheck contract: every schema table is probed and only statuses are formatted. final class FinalScopeFakePdo extends PDO { /** @var list */ public array $queries = []; public function __construct() {} public function query(string $query, ?int $fetchMode = null, mixed ...$fetchModeArgs): PDOStatement|false { $this->queries[] = $query; return false; } } $fakePdo = new FinalScopeFakePdo(); final_scope_assert(deployment_check_schema($fakePdo), 'Healthcheck should probe the required schema without leaking exceptions.'); $probed = array_map(static fn(string $query): string => trim(str_replace(['SELECT 1 FROM', '`', 'LIMIT 1'], '', $query)), $fakePdo->queries); preg_match_all('/CREATE TABLE IF NOT EXISTS ([a-z0-9_]+)/i', $schema, $matches); $schemaTables = array_values(array_unique(array_map('strtolower', $matches[1]))); final_scope_same($schemaTables, $probed, 'Production healthcheck must cover exactly the current schema tables.'); $formatted = deployment_format_check_report(['DB_PASSWORD' => true, 'schema' => false]); final_scope_same(['DB_PASSWORD' => 'OK', 'schema' => 'FAIL'], $formatted, 'Healthcheck output must contain statuses, not values.'); $checks += 2; printf("Final-scope integration tests: %d passed\n", $checks);