94 lines
3.5 KiB
PHP
94 lines
3.5 KiB
PHP
<?php
|
|||
|
|
declare(strict_types=1);
|
||
|
|
|
||
|
|
namespace App\Domain\Jobcard;
|
||
|
|
|
||
|
|
require_once __DIR__ . '/TimeEntryValidator.php';
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Validates and normalizes a framework-free command for recording jobcard time.
|
||
|
|
*/
|
||
|
|
final class TimeEntryCommand
|
||
|
|
{
|
||
|
|
public const MAX_NOTES_LENGTH = 10000;
|
||
|
|
|
||
|
|
public function __construct(private readonly ?TimeEntryValidator $timeEntries = null)
|
||
|
|
{
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* @return array{valid: bool, jobcard_id: ?int, technician_id: ?int, work_date: string, start_time: ?string, end_time: ?string, hours: ?float, notes: ?string, counts_toward_sla: bool, errors: array<string, string>}
|
||
|
|
*/
|
||
|
|
public function validate(array $command): array
|
||
|
|
{
|
||
|
|
$jobcardId = $this->positiveInteger($command['jobcard_id'] ?? null);
|
||
|
|
$technicianId = $this->positiveInteger($command['technician_id'] ?? null);
|
||
|
|
$workDate = $this->text($command['work_date'] ?? null) ?? '';
|
||
|
|
$startTime = $this->text($command['start_time'] ?? null);
|
||
|
|
$endTime = $this->text($command['end_time'] ?? null);
|
||
|
|
$notes = $this->text($command['notes'] ?? null);
|
||
|
|
$slaFlag = $this->boolean($command['counts_toward_sla'] ?? true);
|
||
|
|
$manualHours = $command['hours'] ?? null;
|
||
|
|
if ($manualHours === '') $manualHours = null;
|
||
|
|
|
||
|
|
$entry = ($this->timeEntries ?? new TimeEntryValidator())->validate([
|
||
|
|
'work_date' => $workDate,
|
||
|
|
'start_time' => $startTime,
|
||
|
|
'end_time' => $endTime,
|
||
|
|
'hours' => $manualHours,
|
||
|
|
]);
|
||
|
|
$errors = $entry['errors'];
|
||
|
|
if ($jobcardId === null) $errors['jobcard_id'] = 'Jobcard ID must be a positive integer.';
|
||
|
|
if ($technicianId === null) $errors['technician_id'] = 'Technician ID must be a positive integer.';
|
||
|
|
if ($manualHours !== null && ($startTime !== null || $endTime !== null)) $errors['time'] = 'Manual hours and start/end times cannot both be supplied.';
|
||
|
|
if ($notes !== null && mb_strlen($notes) > self::MAX_NOTES_LENGTH) $errors['notes'] = 'Notes are too long.';
|
||
|
|
if ($slaFlag === null) $errors['counts_toward_sla'] = 'SLA flag must be boolean.';
|
||
|
|
|
||
|
|
return [
|
||
|
|
'valid' => $errors === [],
|
||
|
|
'jobcard_id' => $jobcardId,
|
||
|
|
'technician_id' => $technicianId,
|
||
|
|
'work_date' => $workDate,
|
||
|
|
'start_time' => $startTime,
|
||
|
|
'end_time' => $endTime,
|
||
|
|
'hours' => $entry['hours'],
|
||
|
|
'notes' => $notes,
|
||
|
|
'counts_toward_sla' => $slaFlag ?? false,
|
||
|
|
'errors' => $errors,
|
||
|
|
];
|
||
|
|
}
|
||
|
|
|
||
|
|
private function positiveInteger(mixed $value): ?int
|
||
|
|
{
|
||
|
|
if (is_int($value)) {
|
||
|
|
return $value > 0 ? $value : null;
|
||
|
|
}
|
||
|
|
if (is_string($value) && preg_match('/^[1-9]\d*$/', $value) === 1) {
|
||
|
|
$integer = filter_var($value, FILTER_VALIDATE_INT);
|
||
|
|
return $integer !== false ? $integer : null;
|
||
|
|
}
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
|
||
|
|
private function boolean(mixed $value): ?bool
|
||
|
|
{
|
||
|
|
if (is_bool($value)) return $value;
|
||
|
|
if ($value === 1 || $value === 0) return $value === 1;
|
||
|
|
if (is_string($value)) {
|
||
|
|
return match (strtolower(trim($value))) {
|
||
|
|
'1', 'true', 'yes', 'on' => true,
|
||
|
|
'0', 'false', 'no', 'off', '' => false,
|
||
|
|
default => null,
|
||
|
|
};
|
||
|
|
}
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
|
||
|
|
private function text(mixed $value): ?string
|
||
|
|
{
|
||
|
|
if (!is_scalar($value)) return null;
|
||
|
|
$text = trim((string) $value);
|
||
|
|
return $text === '' ? null : $text;
|
||
|
|
}
|
||
|
|
}
|