89 lines
6.2 KiB
PHP
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);
|
|
});
|
|
}
|
|
}
|