86 lines
3.7 KiB
PHP
86 lines
3.7 KiB
PHP
<?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));
|
|
}
|
|
}
|