*/ public function validateEdit(mixed $id, array $input, array $existingContacts = []): array { $normalizedId = $this->id($id); $result = ($this->contacts ?? new ContactUpdateCommand())->validateForEdit($id, $input, $existingContacts); if ($normalizedId === null) { $result['errors']['id'] = 'Contact ID must be a positive integer.'; } else { $target = $this->find($normalizedId, $existingContacts); if ($target === null) $result['errors']['id'] = 'Contact was not found.'; else { $oldClient = $this->id($target['client_id'] ?? null); $newClient = $this->id($input['client_id'] ?? null); if ($oldClient !== null && $newClient !== $oldClient) $result['errors']['client_id'] = 'Contact client ID cannot be changed during edit.'; } } $result['id'] = $normalizedId; $result['valid'] = $result['errors'] === []; $result['action'] = 'edit'; $result['audit'] = ['event' => 'client_contact_updated', 'entity_type' => 'client_contact', 'entity_id' => $normalizedId, 'client_id' => $result['client_id'] ?? null]; return $result; } /** Alias matching the create/update command vocabulary. */ public function validateForEdit(mixed $id, array $input, array $existingContacts = []): array { return $this->validateEdit($id, $input, $existingContacts); } /** @return array */ public function validate(array $input, array $existingContacts = [], mixed $currentId = null): array { return $currentId === null ? ($this->contacts ?? new ContactUpdateCommand())->validateForCreate($input, $existingContacts) : $this->validateEdit($currentId, $input, $existingContacts); } /** * Validate logical deletion. A primary is promoted to the lowest remaining * contact for the same client; deleting the sole contact is not allowed. * @return array */ public function validateDelete(mixed $id, array $existingContacts = []): array { $normalizedId = $this->id($id); $errors = []; $target = null; foreach ($existingContacts as $contact) { if ($normalizedId !== null && is_array($contact) && $this->id($contact['id'] ?? null) === $normalizedId) { $target = $contact; break; } } if ($normalizedId === null) $errors['id'] = 'Contact ID must be a positive integer.'; if ($target === null) { if ($normalizedId !== null) $errors['id'] = 'Contact was not found.'; return ['valid' => false, 'action' => 'delete', 'id' => $normalizedId, 'client_id' => null, 'replacement_primary_contact_id' => null, 'errors' => $errors, 'audit' => null]; } $clientId = $this->id($target['client_id'] ?? null); if ($clientId === null) { $errors['client_id'] = 'Contact client ID must be a positive integer.'; return ['valid' => false, 'id' => $id, 'client_id' => null, 'replacement_primary_contact_id' => null, 'errors' => $errors]; } $sameClient = array_values(array_filter($existingContacts, fn ($row): bool => is_array($row) && $this->id($row['client_id'] ?? null) === $clientId && $this->id($row['id'] ?? null) !== $normalizedId)); $replacement = null; if (count($sameClient) === 0) { $errors['delete'] = 'The only contact cannot be deleted; add another contact first.'; } elseif ($this->boolean($target['is_primary'] ?? false)) { usort($sameClient, fn (array $a, array $b): int => ($this->id($a['id'] ?? null) ?? PHP_INT_MAX) <=> ($this->id($b['id'] ?? null) ?? PHP_INT_MAX)); $replacement = isset($sameClient[0]) ? $this->id($sameClient[0]['id'] ?? null) : null; if ($replacement === null) $errors['delete'] = 'The only contact cannot be deleted; add another contact first.'; } return ['valid' => $errors === [], 'action' => 'delete', 'id' => $normalizedId, 'client_id' => $clientId, 'replacement_primary_contact_id' => $replacement, 'errors' => $errors, 'audit' => $errors === [] ? ['event' => 'client_contact_deleted', 'entity_type' => 'client_contact', 'entity_id' => $normalizedId, 'client_id' => $clientId, 'replacement_primary_contact_id' => $replacement] : null]; } public function validateForDelete(mixed $id, array $existingContacts = []): array { return $this->validateDelete($id, $existingContacts); } /** @return array */ public function delete(mixed $id, array $existingContacts = []): array { return $this->validateDelete($id, $existingContacts); } /** @return array */ public function validatePrimary(mixed $id, array $existingContacts = []): array { $normalizedId = $this->id($id); foreach ($existingContacts as $row) { if ($normalizedId !== null && is_array($row) && $this->id($row['id'] ?? null) === $normalizedId) { $clientId = $this->id($row['client_id'] ?? null); $demote = []; foreach ($existingContacts as $other) if (is_array($other) && $this->id($other['client_id'] ?? null) === $clientId && $this->id($other['id'] ?? null) !== $normalizedId && $this->boolean($other['is_primary'] ?? false)) $demote[] = $this->id($other['id'] ?? null); return ['valid' => true, 'action' => 'set_primary', 'id' => $normalizedId, 'client_id' => $clientId, 'replace_primary_contact_ids' => array_values(array_filter($demote)), 'errors' => [], 'audit' => ['event' => 'client_contact_primary_set', 'entity_type' => 'client_contact', 'entity_id' => $normalizedId, 'client_id' => $clientId, 'demoted_contact_ids' => array_values(array_filter($demote))]]; } } return ['valid' => false, 'action' => 'set_primary', 'id' => $normalizedId, 'client_id' => null, 'replace_primary_contact_ids' => [], 'errors' => ['id' => $normalizedId === null ? 'Contact ID must be a positive integer.' : 'Contact was not found.'], 'audit' => null]; } /** @return array */ public function setPrimary(mixed $id, array $existingContacts = []): array { return $this->validatePrimary($id, $existingContacts); } private function id(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 boolean(mixed $value): bool { return $value === true || $value === 1 || (is_string($value) && in_array(strtolower(trim($value)), ['1','true','yes','on'], true)); } private function find(int $id, array $rows): ?array { foreach ($rows as $row) if (is_array($row) && $this->id($row['id'] ?? null) === $id) return $row; return null; } }