69 lines
3.3 KiB
PHP
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);
|
|
}
|
|
}
|