*/ 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']; foreach (array_keys($changes) as $field) if (!in_array($field, $allowed, true) && !in_array($field, ['jobcard_id', 'technician_id'], true)) $errors[$field] = "{$field} cannot be changed during correction."; $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; $changed = []; foreach ($allowed as $field) if (array_key_exists($field, $changes) && ($existing[$field] ?? null) !== ($entry[$field] ?? null)) $changed[] = $field; return ['valid' => $errors === [], 'action' => 'correct', 'id' => $id, 'entry' => $entry, 'errors' => $errors, 'audit' => ['event' => 'time_entry_corrected', 'entity_type' => 'time_entry', 'entity_id' => $id, 'changed_fields' => $changed, 'before' => $existing, 'after' => $entry]]; } /** @return array */ public function validate(array $existing, array $changes = []): array { return $this->validateCorrection($existing, $changes); } /** @return array */ public function validateForCorrection(array $existing, array $changes = []): array { return $this->validateCorrection($existing, $changes); } /** @return array */ 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, 'audit' => $errors === [] ? ['event' => 'time_entry_voided', 'entity_type' => 'time_entry', 'entity_id' => $id, 'void_reason' => $reason] : null]; } public function void(array $existing, array $input = []): array { return $this->validateVoid($existing, $input); } /** @return array */ 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)); } }