Files
Investidor-V2/app/Services/InvestmentProfileCommands.php
T

69 lines
3.3 KiB
PHP

<?php
namespace App\Services;
use App\Models\User;
use Illuminate\Support\Facades\Crypt;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;
/** Idempotent self-service commands, serialized by the caller's existing user row lock. */
final class InvestmentProfileCommands
{
private const SOURCE = 'investment-workspace-command';
/**
* @param array<string,mixed> $command
* @return array{version:int,id?:string,country?:string}|null
*/
public function replay(User $actor, ?string $key, array $command): ?array
{
if ($key === null) {
return null;
}
$event = DB::table('audit_events')->where('source_system', self::SOURCE)->where('actor_user_id', $actor->id)->where('correlation_id', strtolower($key))->first();
if (! $event) {
return null;
}
$data = json_decode($event->after_data, true, 512, JSON_THROW_ON_ERROR);
abort_unless(is_array($data) && is_string($data['command_encrypted'] ?? null) && hash_equals(Crypt::decryptString($data['command_encrypted']), $this->canonical($command)), 409,
'Este pedido já foi utilizado com outros dados. Consulte o registo guardado antes de preparar outro pedido.');
$result = is_string($data['result_encrypted'] ?? null) ? json_decode(Crypt::decryptString($data['result_encrypted']), true, 512, JSON_THROW_ON_ERROR) : ($data['result'] ?? null);
if (! is_array($result) || ! is_int($result['version'] ?? null) || $result['version'] < 1
|| (array_key_exists('id', $result) && (! is_string($result['id']) || ! Str::isUuid($result['id'])))
|| (array_key_exists('country', $result) && (! is_string($result['country']) || preg_match('/^[A-Z]{2}$/D', $result['country']) !== 1))) {
throw new \LogicException('Stored workspace command result unavailable.');
}
$receipt = isset($result['id']) ? ['id' => $result['id'], 'version' => $result['version']] : ['version' => $result['version']];
if (isset($result['country'])) {
$receipt['country'] = $result['country'];
}
return $receipt;
}
/** @param array<string,mixed> $command
* @param array{version:int,id?:string,country?:string} $result */
public function record(User $actor, ?string $key, array $command, array $result): void
{
if ($key === null) {
return;
}
DB::table('audit_events')->insert(['id' => (string) Str::uuid(), 'source_system' => self::SOURCE, 'actor_user_id' => $actor->id,
'entity_type' => 'investment-profile-command', 'entity_id' => $result['id'] ?? null, 'action' => 'command_applied', 'correlation_id' => strtolower($key), 'data_classification' => 'restricted',
'after_data' => json_encode(['command_encrypted' => Crypt::encryptString($this->canonical($command)), ...(array_key_exists('country', $result) ? ['result_encrypted' => Crypt::encryptString(json_encode($result, JSON_THROW_ON_ERROR))] : ['result' => $result])], JSON_THROW_ON_ERROR)]);
}
/** @param array<string,mixed> $command */
private function canonical(array $command): string
{
if (is_array($command['payload'] ?? null)) {
ksort($command['payload']);
}
ksort($command);
return json_encode($command, JSON_THROW_ON_ERROR);
}
}