2026-09-01 21:03:38 +02:00
<? 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' ];
2026-09-01 21:47:35 +02:00
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." ;
2026-09-01 21:03:38 +02:00
$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 ;
2026-09-01 21:47:35 +02:00
$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 ]];
2026-09-01 21:03:38 +02:00
}
/** @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.' ;
2026-09-01 21:47:35 +02:00
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 ];
2026-09-01 21:03:38 +02:00
}
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 ));
}
}