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

89 lines
6.2 KiB
PHP

<?php
namespace App\Services;
use App\Models\User;
use Illuminate\Support\Arr;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
use Illuminate\Support\Facades\Validator;
use Illuminate\Validation\Rule;
final class DeskRequestCheckpoints
{
public function installed(): bool
{
return Schema::hasTable('desk_request_checkpoints');
}
/** @return array<string,mixed> */
public function show(User $actor, string $id): array
{
$desk = app(VirtualDesk::class);
$request = $desk->request($actor, $id, true);
$case = $desk->case($actor, $request->case_id, true);
$row = $this->installed() ? DB::table('desk_request_checkpoints')->where('request_id', $id)->where('actor_id', $actor->id)->first() : null;
return ['installed' => $this->installed(), 'request' => ['id' => $id, 'version' => (int) $request->version, 'status' => $request->status,
'protocol' => $request->protocol, 'analyst_id' => $request->analyst_id, 'due_at' => $request->due_at,
'editable_fields' => json_decode($request->editable_fields ?? '[]', true), 'payload' => $desk->decode($request->payload),
'can_edit' => ! $case->closed_at && in_array($request->status, ['draft', 'returned'], true)],
'checkpoint' => $row ? ['version' => (int) $row->version, 'request_version' => (int) $row->request_version,
'updated_at' => $row->updated_at, ...$desk->decode($row->snapshot)] : null];
}
/** @param array<string,mixed> $input
* @return array<string,mixed> */
public function save(User $actor, string $id, array $input): array
{
abort_unless($this->installed(), 409, 'A gravação automática requer a actualização de rascunhos. A gravação manual continua disponível.');
$data = Validator::make(['checkpoint' => $input], [
'checkpoint' => 'required|array:version,request_version,operation_key,payload,edited_keys,reason',
'checkpoint.version' => 'required|integer|min:0', 'checkpoint.request_version' => 'required|integer|min:1',
'checkpoint.operation_key' => 'required|uuid', 'checkpoint.payload' => 'present|array',
'checkpoint.edited_keys' => 'present|array|list|max:65', 'checkpoint.edited_keys.*' => 'required|string|distinct|max:100',
'checkpoint.reason' => 'nullable|string|max:3000',
])->validate()['checkpoint'];
return DB::transaction(function () use ($actor, $id, $data): array {
$desk = app(VirtualDesk::class);
$request = $desk->request($actor, $id, true, true);
$case = $desk->case($actor, $request->case_id, true, true);
abort_unless(! $case->closed_at && in_array($request->status, ['draft', 'returned'], true), 409, 'Este pedido já não permite alterar o rascunho.');
abort_unless((int) $request->version === (int) $data['request_version'], 409, 'O pedido mudou. Compare a versão actual antes de recuperar as suas alterações.');
$payload = $desk->checkpointFields($data['payload'], $desk->definition($request));
app(DeskAttachments::class)->snapshot($request, $payload, $desk->definition($request), false);
$allowed = [...array_keys($desk->definition($request)['fields']), 'country', 'declared_origin_country_code', 'context_reuse', 'context_opportunity', 'context_opportunity_snapshot'];
Validator::make($data, ['edited_keys.*' => [Rule::in($allowed)]])->validate();
$base = $desk->decode($request->payload);
$edited = array_values(array_unique([...$data['edited_keys'], ...array_filter(array_unique([...array_keys($base), ...array_keys($payload)]), fn ($key) => ($base[$key] ?? null) !== ($payload[$key] ?? null))]));
if ($request->status === 'returned') {
$editable = json_decode($request->editable_fields ?? '[]', true);
foreach (array_unique([...array_keys($base), ...array_keys($payload), ...$data['edited_keys']]) as $key) {
abort_if(! in_array($key, $editable, true) && ((($base[$key] ?? null) !== ($payload[$key] ?? null)) || in_array($key, $data['edited_keys'], true)), 422, 'O rascunho só pode corrigir os campos autorizados.');
}
}
$operationHash = hash('sha256', json_encode(Arr::sortRecursive(['version' => (int) $data['version'], 'request_version' => (int) $data['request_version'],
'operation_key' => $data['operation_key'], 'payload' => $payload, 'edited_keys' => $data['edited_keys'], 'reason' => $data['reason'] ?? '']), JSON_THROW_ON_ERROR));
$row = DB::table('desk_request_checkpoints')->where('request_id', $id)->where('actor_id', $actor->id)->lockForUpdate()->first();
if ($row && $row->operation_key === $data['operation_key']) {
abort_unless(hash_equals($row->operation_sha256, $operationHash), 409, 'Esta tentativa já foi usada com outros dados.');
return $this->show($actor, $id);
}
abort_unless((int) ($row->version ?? 0) === (int) $data['version'], 409, 'O rascunho mudou noutro separador. Compare antes de substituir.');
$version = (int) ($row->version ?? 0) + 1;
DB::table('desk_request_checkpoints')->updateOrInsert(['request_id' => $id, 'actor_id' => $actor->id], [
'version' => $version, 'request_version' => $request->version, 'operation_key' => $data['operation_key'], 'operation_sha256' => $operationHash,
'snapshot' => $desk->encode(['payload' => $payload, 'base_payload' => $base, 'edited_keys' => $edited, 'reason' => $data['reason'] ?? '']),
'created_at' => $row->created_at ?? now(), 'updated_at' => now(),
]);
DB::table('audit_events')->insert(['source_system' => 'virtual-desk', 'entity_type' => 'desk_request_checkpoints', 'entity_id' => $id,
'actor_user_id' => $actor->id, 'action' => 'draft_checkpoint_saved', 'data_classification' => 'restricted',
'after_data' => json_encode(['version' => $version, 'request_version' => (int) $request->version], JSON_THROW_ON_ERROR)]);
return $this->show($actor, $id);
});
}
}