2026-09-01 21:03:38 +02:00
<? php
declare ( strict_types = 1 );
namespace App\Domain\Client ;
require_once __DIR__ . '/ContactUpdateCommand.php' ;
/**
* Storage-agnostic command for safe contact edits, primary promotion and deletion.
* Persistence adapters can apply the returned replacement IDs transactionally.
*/
final class ContactEditCommand
{
public function __construct ( private readonly ? ContactUpdateCommand $contacts = null )
{
}
/** @return array<string,mixed> */
2026-09-01 21:47:35 +02:00
public function validateEdit ( mixed $id , array $input , array $existingContacts = []) : array
2026-09-01 21:03:38 +02:00
{
2026-09-01 21:47:35 +02:00
$normalizedId = $this -> id ( $id );
2026-09-01 21:03:38 +02:00
$result = ( $this -> contacts ?? new ContactUpdateCommand ()) -> validateForEdit ( $id , $input , $existingContacts );
2026-09-01 21:47:35 +02:00
if ( $normalizedId === null ) {
2026-09-01 21:03:38 +02:00
$result [ 'errors' ][ 'id' ] = 'Contact ID must be a positive integer.' ;
2026-09-01 21:47:35 +02:00
} 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.' ;
}
2026-09-01 21:03:38 +02:00
}
2026-09-01 21:47:35 +02:00
$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 ];
2026-09-01 21:03:38 +02:00
return $result ;
}
/** Alias matching the create/update command vocabulary. */
2026-09-01 21:47:35 +02:00
public function validateForEdit ( mixed $id , array $input , array $existingContacts = []) : array
2026-09-01 21:03:38 +02:00
{
return $this -> validateEdit ( $id , $input , $existingContacts );
}
/** @return array<string,mixed> */
2026-09-01 21:47:35 +02:00
public function validate ( array $input , array $existingContacts = [], mixed $currentId = null ) : array
2026-09-01 21:03:38 +02:00
{
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<string,mixed>
*/
2026-09-01 21:47:35 +02:00
public function validateDelete ( mixed $id , array $existingContacts = []) : array
2026-09-01 21:03:38 +02:00
{
2026-09-01 21:47:35 +02:00
$normalizedId = $this -> id ( $id );
2026-09-01 21:03:38 +02:00
$errors = [];
$target = null ;
foreach ( $existingContacts as $contact ) {
2026-09-01 21:47:35 +02:00
if ( $normalizedId !== null && is_array ( $contact ) && $this -> id ( $contact [ 'id' ] ?? null ) === $normalizedId ) {
2026-09-01 21:03:38 +02:00
$target = $contact ;
break ;
}
}
2026-09-01 21:47:35 +02:00
if ( $normalizedId === null ) $errors [ 'id' ] = 'Contact ID must be a positive integer.' ;
2026-09-01 21:03:38 +02:00
if ( $target === null ) {
2026-09-01 21:47:35 +02:00
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 ];
2026-09-01 21:03:38 +02:00
}
$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 ];
}
2026-09-01 21:47:35 +02:00
$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 ));
2026-09-01 21:03:38 +02:00
$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.' ;
}
2026-09-01 21:47:35 +02:00
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 ];
2026-09-01 21:03:38 +02:00
}
2026-09-01 21:47:35 +02:00
public function validateForDelete ( mixed $id , array $existingContacts = []) : array
2026-09-01 21:03:38 +02:00
{
return $this -> validateDelete ( $id , $existingContacts );
}
/** @return array<string,mixed> */
2026-09-01 21:47:35 +02:00
public function delete ( mixed $id , array $existingContacts = []) : array
2026-09-01 21:03:38 +02:00
{
return $this -> validateDelete ( $id , $existingContacts );
}
/** @return array<string,mixed> */
2026-09-01 21:47:35 +02:00
public function validatePrimary ( mixed $id , array $existingContacts = []) : array
2026-09-01 21:03:38 +02:00
{
2026-09-01 21:47:35 +02:00
$normalizedId = $this -> id ( $id );
2026-09-01 21:03:38 +02:00
foreach ( $existingContacts as $row ) {
2026-09-01 21:47:35 +02:00
if ( $normalizedId !== null && is_array ( $row ) && $this -> id ( $row [ 'id' ] ?? null ) === $normalizedId ) {
2026-09-01 21:03:38 +02:00
$clientId = $this -> id ( $row [ 'client_id' ] ?? null );
$demote = [];
2026-09-01 21:47:35 +02:00
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 ))]];
2026-09-01 21:03:38 +02:00
}
}
2026-09-01 21:47:35 +02:00
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 ];
2026-09-01 21:03:38 +02:00
}
/** @return array<string,mixed> */
2026-09-01 21:47:35 +02:00
public function setPrimary ( mixed $id , array $existingContacts = []) : array
2026-09-01 21:03:38 +02:00
{
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 )); }
2026-09-01 21:47:35 +02:00
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 ;
}
2026-09-01 21:03:38 +02:00
}