*/ 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 $input * @return array */ 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); }); } }