feat: add administration reporting and correction services

This commit is contained in:
Marco0300
2026-09-01 21:03:38 +02:00
parent de2bf277c4
commit b983f90dcb
24 changed files with 1392 additions and 31 deletions
@@ -0,0 +1,85 @@
<?php
declare(strict_types=1);
namespace App\Domain\Jobcard;
require_once __DIR__ . '/TimeEntryCommand.php';
/**
* Validates immutable time-entry corrections and logical voids.
* No database deletion is represented: adapters should persist the returned
* action in a transaction and retain the original entry for auditability.
*/
final class TimeEntryCorrectionCommand
{
public function __construct(private readonly ?TimeEntryCommand $entries = null)
{
}
/** @return array<string,mixed> */
public function validateCorrection(array $existing, array $changes): array
{
$id = $this->positiveId($existing['id'] ?? null);
$errors = $id === null ? ['id' => 'Time-entry ID must be a positive integer.'] : [];
if ($this->isVoided($existing)) $errors['voided'] = 'A voided time entry cannot be corrected.';
foreach (['jobcard_id', 'technician_id'] as $field) {
if (array_key_exists($field, $changes) && $this->positiveId($changes[$field]) !== $this->positiveId($existing[$field] ?? null)) $errors[$field] = "{$field} cannot be changed during correction.";
}
$allowed = ['work_date', 'start_time', 'end_time', 'hours', 'notes', 'counts_toward_sla'];
$payload = $existing;
foreach ($allowed as $field) if (array_key_exists($field, $changes)) $payload[$field] = $changes[$field];
$validated = ($this->entries ?? new TimeEntryCommand())->validate($payload);
$errors = [...$validated['errors'], ...$errors];
$entry = [...$payload, ...$validated];
unset($entry['valid'], $entry['errors']);
$entry['id'] = $id;
return ['valid' => $errors === [], 'action' => 'correct', 'id' => $id, 'entry' => $entry, 'errors' => $errors];
}
/** @return array<string,mixed> */
public function validate(array $existing, array $changes = []): array
{
return $this->validateCorrection($existing, $changes);
}
/** @return array<string,mixed> */
public function validateForCorrection(array $existing, array $changes = []): array
{
return $this->validateCorrection($existing, $changes);
}
/** @return array<string,mixed> */
public function validateVoid(array $existing, array $input = []): array
{
$id = $this->positiveId($existing['id'] ?? null);
$errors = $id === null ? ['id' => 'Time-entry ID must be a positive integer.'] : [];
if ($this->isVoided($existing)) $errors['voided'] = 'Time entry is already voided.';
$reason = is_scalar($input['reason'] ?? null) ? trim((string)$input['reason']) : '';
if ($reason === '') $errors['reason'] = 'A void reason is required.';
elseif (mb_strlen($reason) > 1000) $errors['reason'] = 'Void reason must be 1000 characters or fewer.';
return ['valid' => $errors === [], 'action' => 'void', 'id' => $id, 'void_reason' => $reason === '' ? null : $reason, 'entry' => $existing, 'errors' => $errors];
}
public function void(array $existing, array $input = []): array
{
return $this->validateVoid($existing, $input);
}
/** @return array<string,mixed> */
public function validateForVoid(array $existing, array $input = []): array
{
return $this->validateVoid($existing, $input);
}
private function positiveId(mixed $value): ?int
{
if (is_int($value) && $value > 0) return $value;
if (is_string($value) && preg_match('/^[1-9]\d*$/', trim($value)) === 1) return filter_var(trim($value), FILTER_VALIDATE_INT) ?: null;
return null;
}
private function isVoided(array $entry): bool
{
$value = $entry['voided'] ?? false;
return $value === true || $value === 1 || (is_string($value) && in_array(strtolower(trim($value)), ['1','true','yes'], true));
}
}