Files
breadcrumb-the-shire/modules/security/lib/Module/Security/Service/SecurityCheckProcess.php

271 lines
10 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
namespace MintyPHP\Module\Security\Service;
/**
* The fixed internal security-check process.
*
* Identical for every check, so it lives in code (not the DB). Step labels and
* descriptions are translation keys — call t() at render time. The tech-checklist
* step (perform_check) expands into the per-product items from the check's
* tech_schema_snapshot.
*
* Pure logic only (no DB, no HTTP) — fully unit-testable.
*
* @api Consumed by Security pages, views and SecurityCheckService.
*/
final class SecurityCheckProcess
{
public const STATUS_OPEN = 'open';
public const STATUS_IN_PROGRESS = 'in_progress';
public const STATUS_COMPLETED = 'completed';
public const STATUS_ARCHIVED = 'archived';
public const STEP_INFORMATION = 'information_gathering';
public const STEP_PERFORM_CHECK = 'perform_check';
public const STEP_REPORT = 'report';
public const KIND_STEP = 'step';
public const KIND_INFO = 'info';
public const KIND_TECH_CHECKLIST = 'tech_checklist';
public const FIELD_TEXTAREA = 'textarea';
public const FIELD_DATETIME = 'datetime';
/**
* Ordered fixed process steps.
*
* @return list<array{key: string, label: string, description: string, kind: string, fields?: list<array{key: string, label: string, type: string, required: bool}>}>
*/
public function steps(): array
{
return [
[
'key' => 'beauftragung',
'label' => 'Order received',
'description' => 'The security check was sold for one or more customer projects; sales briefs the security officer with all details.',
'kind' => self::KIND_STEP,
],
[
'key' => self::STEP_INFORMATION,
'label' => 'Gather information',
'description' => 'Collect all relevant information from Customer Care / the project lead.',
'kind' => self::KIND_INFO,
'fields' => [
['key' => 'technical_contact', 'label' => 'Technical contact at customer', 'type' => self::FIELD_TEXTAREA, 'required' => true],
['key' => 'deployment', 'label' => 'Deployment location & type', 'type' => self::FIELD_TEXTAREA, 'required' => true],
['key' => 'system_info', 'label' => 'Technical system information', 'type' => self::FIELD_TEXTAREA, 'required' => true],
['key' => 'external_systems', 'label' => 'External systems / API endpoints (exact URLs)', 'type' => self::FIELD_TEXTAREA, 'required' => true],
['key' => 'special_notes', 'label' => 'Special notes', 'type' => self::FIELD_TEXTAREA, 'required' => false],
],
],
[
'key' => 'scheduling',
'label' => 'Scheduling',
'description' => 'Agree internally and with the customer on the check date. Only the 23h go-live window is relevant to the customer.',
'kind' => self::KIND_STEP,
'fields' => [
['key' => 'golive_start', 'label' => 'Go-live window start', 'type' => self::FIELD_DATETIME, 'required' => true],
['key' => 'golive_end', 'label' => 'Go-live window end', 'type' => self::FIELD_DATETIME, 'required' => true],
],
],
[
'key' => 'blocker_appointment',
'label' => 'Internal blocker appointment',
'description' => 'Create a 12 day internal blocker; invite Customer Care, Customizing and Sales.',
'kind' => self::KIND_STEP,
'fields' => [
['key' => 'blocker_start', 'label' => 'Blocker start', 'type' => self::FIELD_DATETIME, 'required' => true],
['key' => 'blocker_end', 'label' => 'Blocker end', 'type' => self::FIELD_DATETIME, 'required' => true],
],
],
[
'key' => self::STEP_PERFORM_CHECK,
'label' => 'Perform security check',
'description' => 'Work through the product-specific technical checklist.',
'kind' => self::KIND_TECH_CHECKLIST,
],
[
'key' => 'report',
'label' => 'Create report & notify customer',
'description' => 'Create the report for the customer and notify them.',
'kind' => self::KIND_STEP,
],
];
}
/**
* Keys of steps completed manually (everything except the tech-checklist step).
*
* @return list<string>
*/
public function manualStepKeys(): array
{
$keys = [];
foreach ($this->steps() as $step) {
if ($step['kind'] !== self::KIND_TECH_CHECKLIST) {
$keys[] = $step['key'];
}
}
return $keys;
}
/**
* Structured fields captured on a given step (info textareas, datetimes, …).
*
* @return list<array{key: string, label: string, type: string, required: bool}>
*/
public function stepFields(string $stepKey): array
{
foreach ($this->steps() as $step) {
if ($step['key'] === $stepKey) {
return $step['fields'] ?? [];
}
}
return [];
}
/**
* Keys of a step's fields that must be filled before it can be completed.
* (E.g. "Special notes" and the free-text note stay optional.)
*
* @return list<string>
*/
public function requiredFieldKeys(string $stepKey): array
{
$keys = [];
foreach ($this->stepFields($stepKey) as $field) {
if ($field['required']) {
$keys[] = $field['key'];
}
}
return $keys;
}
/**
* Extract the checkable items from a tech-schema snapshot.
*
* @param array<string, mixed> $techSnapshot
* @return list<array{key: string, label: string}>
*/
public static function techCheckItems(array $techSnapshot): array
{
$items = is_array($techSnapshot['items'] ?? null) ? $techSnapshot['items'] : [];
$checks = [];
foreach ($items as $item) {
if (!is_array($item)) {
continue;
}
if (($item['type'] ?? '') === 'check' && trim((string) ($item['key'] ?? '')) !== '') {
$checks[] = ['key' => (string) $item['key'], 'label' => (string) ($item['label'] ?? '')];
}
}
return $checks;
}
/**
* Compute progress + derived status purely from state.
*
* @param array<string, mixed> $processState per-step state keyed by step key
* @param array<string, mixed> $techSnapshot frozen tech-schema snapshot
* @param array<string, mixed> $techState per-item state keyed by item key
* @return array{done: int, total: int, percent: int, manual_done: int, manual_total: int, tech_done: int, tech_total: int, step6_done: bool, status: string}
*/
public function computeProgress(array $processState, array $techSnapshot, array $techState, bool $archived = false): array
{
$manualKeys = $this->manualStepKeys();
$manualTotal = count($manualKeys);
$manualDone = 0;
foreach ($manualKeys as $key) {
if (!empty($processState[$key]['done'])) {
$manualDone++;
}
}
$techItems = self::techCheckItems($techSnapshot);
$techTotal = count($techItems);
$techDone = 0;
foreach ($techItems as $item) {
if (!empty($techState[$item['key']]['done'])) {
$techDone++;
}
}
$step6Done = $techTotal === 0 ? true : ($techDone === $techTotal);
$total = $manualTotal + $techTotal;
$done = $manualDone + $techDone;
$percent = $total > 0 ? (int) floor(($done / $total) * 100) : 0;
if ($archived) {
$status = self::STATUS_ARCHIVED;
} elseif ($total > 0 && $done >= $total) {
$status = self::STATUS_COMPLETED;
} elseif ($done > 0) {
$status = self::STATUS_IN_PROGRESS;
} else {
$status = self::STATUS_OPEN;
}
return [
'done' => $done,
'total' => $total,
'percent' => $percent,
'manual_done' => $manualDone,
'manual_total' => $manualTotal,
'tech_done' => $techDone,
'tech_total' => $techTotal,
'step6_done' => $step6Done,
'status' => $status,
];
}
/**
* Whether every step preceding the final "report" step is complete: all
* manual steps except the report itself are done AND the technical checklist
* is fully ticked. Gates the "Generate PDF customer report" action so the
* report can only be produced once the check has actually been carried out.
*
* @param array<string, mixed> $processState
* @param array<string, mixed> $techSnapshot
* @param array<string, mixed> $techState
*/
public function reportPrerequisitesMet(array $processState, array $techSnapshot, array $techState): bool
{
foreach ($this->manualStepKeys() as $key) {
if ($key === self::STEP_REPORT) {
continue;
}
if (empty($processState[$key]['done'])) {
return false;
}
}
return $this->computeProgress($processState, $techSnapshot, $techState)['step6_done'];
}
public static function statusLabel(string $status): string
{
return match ($status) {
self::STATUS_OPEN => 'Open',
self::STATUS_IN_PROGRESS => 'In progress',
self::STATUS_COMPLETED => 'Completed',
self::STATUS_ARCHIVED => 'Archived',
default => $status,
};
}
public static function statusVariant(string $status): string
{
return match ($status) {
self::STATUS_IN_PROGRESS => 'warning',
self::STATUS_COMPLETED => 'success',
default => 'neutral',
};
}
}