feat: complete jobcard operations and access controls
This commit is contained in:
@@ -0,0 +1,93 @@
|
||||
<?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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user