diff --git a/app/Http/Controllers/InvestmentRelationshipRepairController.php b/app/Http/Controllers/InvestmentRelationshipRepairController.php index f428b907..13a77c6f 100644 --- a/app/Http/Controllers/InvestmentRelationshipRepairController.php +++ b/app/Http/Controllers/InvestmentRelationshipRepairController.php @@ -21,9 +21,10 @@ class InvestmentRelationshipRepairController extends Controller public function apply(Request $request, InvestmentRelationshipRepair $repair): RedirectResponse|JsonResponse { if ($request->input('operation') === 'traceability') { - $data = $request->validate(['hash' => ['required', 'string', 'size:64'], 'reverse' => ['required', 'boolean']]); + $data = $request->validate(['hash' => ['required', 'string', 'size:64'], 'reverse' => ['required', 'boolean'], + 'reason' => ['required_if:reverse,true', 'nullable', 'string', 'max:1000'], 'confirm_reverse' => ['accepted_if:reverse,true']]); try { - $result = app(InvestmentTerritorialTraceability::class)->apply($data['hash'], $request->user()->id, $data['reverse']); + $result = app(InvestmentTerritorialTraceability::class)->apply($data['hash'], $request->user()->id, $data['reverse'], $data['reason'] ?? null); } catch (HttpException $exception) { if ($request->expectsJson()) { throw $exception; diff --git a/app/Services/InvestmentTerritorialTraceability.php b/app/Services/InvestmentTerritorialTraceability.php index 401de5b2..80d0fd2a 100644 --- a/app/Services/InvestmentTerritorialTraceability.php +++ b/app/Services/InvestmentTerritorialTraceability.php @@ -37,7 +37,12 @@ class InvestmentTerritorialTraceability $last = DB::table('audit_events')->where('source_system', 'territory-traceability-v2')->where('after_data->plan_hash', $hash)->orderByDesc('occurred_at')->orderByDesc('id')->first(); return ['hash' => $hash, 'plan' => $plan, 'code_matches' => ($plan['code_hash'] ?? '') === $this->fingerprint(), - 'applied' => $last?->action === 'traceability_applied', 'backup_ready' => $this->backupReady($plan['target'], $hash)]; + 'applied' => $last?->action === 'traceability_applied', 'reversed' => $last?->action === 'traceability_reversed', + 'history' => DB::table('audit_events')->where('source_system', 'territory-traceability-v2')->orderByDesc('occurred_at')->limit(20)->get(['id', 'action', 'actor_user_id', 'occurred_at', 'after_data'])->map(function ($event) { + $data = json_decode($event->after_data, true); + + return ['id' => $event->id, 'action' => $event->action, 'actor_user_id' => $event->actor_user_id, 'occurred_at' => $event->occurred_at, 'reason' => $data['reason'] ?? null]; + })->all(), 'backup_ready' => $this->backupReady($plan['target'], $hash)]; } /** @param array $target */ @@ -67,8 +72,9 @@ class InvestmentTerritorialTraceability } /** @return array */ - public function apply(string $hash, int $actor, bool $reverse = false): array + public function apply(string $hash, int $actor, bool $reverse = false, ?string $reason = null): array { + abort_if($reverse && trim($reason ?? '') === '', 422, 'Indique o motivo da reversão.'); $preview = $this->preview(); abort_unless($preview && hash_equals($preview['hash'], $hash) && $preview['code_matches'], 409, 'Plano ou código alterado. Reveja a pré-visualização.'); $plan = $preview['plan']; @@ -79,7 +85,7 @@ class InvestmentTerritorialTraceability } abort_unless($preview['backup_ready'], 422, 'Backup/restauro/reversão verificados e atualizados são necessários.'); - return DB::transaction(function () use ($plan, $hash, $actor, $reverse) { + return DB::transaction(function () use ($plan, $hash, $actor, $reverse, $reason) { DB::select('SELECT pg_advisory_xact_lock(81026, 160000)'); $last = DB::table('audit_events')->where('source_system', 'territory-traceability-v2')->where('after_data->plan_hash', $hash)->orderByDesc('occurred_at')->orderByDesc('id')->lockForUpdate()->first(); if ((! $reverse && $last?->action === 'traceability_applied') || ($reverse && $last?->action === 'traceability_reversed')) { @@ -96,6 +102,7 @@ class InvestmentTerritorialTraceability $values = $update['before']; $values['version_no'] = $update['after']['version_no'] + 1; DB::table('cluster_projects')->where('id', $update['id'])->update($values); + $before[] = ['id' => $update['id'], 'before' => $update['after'], 'after' => $values]; } } else { foreach ($plan['guards'] as $guard) { @@ -111,7 +118,7 @@ class InvestmentTerritorialTraceability } DB::table('audit_events')->insert(['source_system' => 'territory-traceability-v2', 'entity_type' => 'territorial_sources', 'actor_user_id' => $actor, 'action' => $reverse ? 'traceability_reversed' : 'traceability_applied', 'data_classification' => 'internal', 'occurred_at' => DB::raw('clock_timestamp()'), - 'after_data' => json_encode(['plan_hash' => $hash, 'rows' => $plan['rows'], 'updates' => $before, 'no_new_geography' => true, 'official_policy' => 'accepted_without_revalidation'], JSON_THROW_ON_ERROR)]); + 'after_data' => json_encode(['plan_hash' => $hash, 'rows' => $plan['rows'], 'updates' => $before, 'reason' => $reverse ? trim($reason) : null, 'no_new_geography' => true, 'official_policy' => 'accepted_without_revalidation'], JSON_THROW_ON_ERROR)]); return ['replayed' => false, 'changed' => $reverse ? count($applied['updates']) : count($before), 'associations' => count($plan['rows'])]; }); diff --git a/docs/investmoz/v2/ativacao-rastreabilidade-territorial.md b/docs/investmoz/v2/ativacao-rastreabilidade-territorial.md index 927e023c..6ead1dcd 100644 --- a/docs/investmoz/v2/ativacao-rastreabilidade-territorial.md +++ b/docs/investmoz/v2/ativacao-rastreabilidade-territorial.md @@ -44,3 +44,25 @@ Provas privadas em `storage/app/private/investmoz-v2`: `territory-activation-pla **Preparado:** código, pré-visualização e aplicação autenticada. **Validado em QA:** aplicação, reversão, permissões e apresentação. **Aplicado na base ativa:** nenhuma alteração de dados deste incremento; ação autenticada do responsável permanece por executar. Continuam separadas: questões jurídicas específicas de âmbito/versão/aplicabilidade ainda não resolvidas; diferença de cobertura do inventário; ClamD real; primeiro pedido real no Balcão. A aceitação da proveniência oficial não foi reaberta e nenhuma dessas pendências foi concluída pelos testes territoriais. + +## Confirmação após as ações autenticadas + +Leitura `READ ONLY` de `investemoz:5432` em 09/10/2026 às 00:10:09 UTC (02:10:09 local). A auditoria regista `traceability_applied` às 02:06:31 e `traceability_reversed` às 02:06:47 locais, ambas pelo utilizador 13 e para o mesmo hash do plano preparado. **Estado atual: incremento revertido; referências propostas não permanecem aplicadas.** + +As 13 alterações registadas no evento de aplicação correspondem exatamente aos valores propostos. Após reversão, os 13 pares de referências regressaram aos valores anteriores e as versões aumentaram duas unidades, preservando o histórico. Estados de validação e tipos de associação mantiveram-se; os 46 registos territoriais, as fontes legais, a hierarquia e a geometria também foram preservados. A projeção de fontes deste plano deixa de estar ativa após reversão. + +Evidência privada: `territory-post-action-confirmation.json`, incluindo identificação das duas ações e comparação por associação. Esta confirmação não realizou escritas na base ativa. Para reaplicar, é necessário preparar uma nova pré-visualização com as versões atuais e provas correspondentes; não repetir o plano revertido nem a aplicação automaticamente. + +## Diagnóstico da reversão e novo plano + +9 de outubro de 2026. O evento antigo de reversão identifica a conta 13, a ação e o plano, mas **não contém motivo**. Não é possível inferir a intenção humana. No código, `traceability_reversed` é registado quando a ação explícita de reversão é executada; uma falha transacional não gera esse evento. A comparação anterior comprovou que aplicação e restituição funcionaram corretamente, sem perda de validações. + +Foi corrigida uma fragilidade de interação: o mesmo botão mudava de aplicar para reverter após sucesso. A reversão passa a exigir um motivo preenchido e a confirmação expressa de restituição das referências, verificados também no servidor. O motivo e os valores restituídos ficam no novo evento de auditoria. O histórico antigo permanece intacto e apresenta «motivo não registado»; não foi preenchido retroativamente. Um plano revertido aparece como tal e o seu botão fica desativado, exigindo novo plano. + +**Nova pré-visualização:** mesma página e ação `Aplicar rastreabilidade territorial V2`; destino `investemoz:5432`, 112 entradas e 13 atualizações, agora com guardas da versão **3** das associações. Hash privado do plano: `ec11b9e400b7b127fbc8d42a2afe5e4d2c8a3919216a5d7439fb38b0751c57c6`. Não cria relações ou geometrias nem reabre a aceitação oficial. + +**Provas atualizadas:** backup/restauro de 288 tabelas e cinco ficheiros; cópia `desk_restore_20261009_001345_test:55441`. Ensaio aplicou 13 referências, confirmou idempotência e reverteu 13, preservando validações e onze tabelas relacionadas. A prova nova substitui a anterior para esta aplicação e expira em **10/10/2026 às 02:13:54 locais**. Os ficheiros do plano e da prova anteriores foram preservados privadamente. + +Os três testes afetados passaram, com 30 asserções, incluindo recusa de reversão sem motivo/confirmação e conservação do motivo no histórico. TypeScript, formatação/lint, PHPStan e build passaram. As pendências de rastreabilidade, jurídicas específicas, cobertura documental, ClamD e primeiro pedido real permanecem separadas. **Não houve reaplicação automática na base ativa.** + +O navegador QA confirmou pré-visualização, aplicação, recuperação e reversão após preencher motivo e confirmação; o botão de reversão estava inicialmente desativado. Sem erros de JavaScript. Leitura ativa final confirmou todas as guardas atuais do novo plano e apenas os dois eventos históricos, sendo o último a reversão. diff --git a/docs/investmoz/v2/backlog-mestre.md b/docs/investmoz/v2/backlog-mestre.md index b8ff3a57..14a16ccd 100644 --- a/docs/investmoz/v2/backlog-mestre.md +++ b/docs/investmoz/v2/backlog-mestre.md @@ -163,3 +163,11 @@ Propostas e [relatório territorial](qualidade-territorial.md) corrigidos para d | QA | 56 testes PHP/884 asserções, quatro testes de reconciliação, TypeScript, lint, PHPStan, build e percurso autenticado no navegador | Zero eventos deste incremento na leitura ativa final; dados territoriais inalterados | Restam pontes de rastreabilidade e seis correspondências oportunidade–âncora/cluster sem fundamento explícito recuperado; não se retirou a aceitação existente. Questões jurídicas específicas, diferença de cobertura do inventário, ClamD real e primeiro pedido real permanecem separadas. + +### Confirmação ativa — aplicação seguida de reversão + +Leitura `READ ONLY` em 09/10/2026 às 02:10:09 locais: auditoria confirma aplicação às 02:06:31 e reversão às 02:06:47, pelo utilizador 13, ambas para o plano preparado. As 13 referências foram aplicadas conforme previsto e depois restituídas, preservando validações, tipos de associação, versões monotónicas e histórico. Os 46 registos, fontes legais e geografia mantiveram-se. **Estado atual V2-08/V2-13: incremento revertido na base ativa**, não instalação permanente concluída. Prova privada: `territory-post-action-confirmation.json`. Nova aplicação exige novo plano para as versões atuais; nenhuma escrita foi repetida durante esta confirmação. + +### Novo plano após reversão — 9 de outubro + +O motivo humano não consta do evento original e não foi inventado. Corrigida a possibilidade de reversão imediata pelo botão que mudava após aplicação: exige agora motivo e confirmação expressa, conservados na auditoria; planos revertidos ficam desativados. Novo plano preparado com as versões 3 atuais das 13 associações, mantendo 112 entradas e validações existentes. Backup/restauro atualizado: 288 tabelas e cinco ficheiros em `desk_restore_20261009_001345_test:55441`; aplicação/idempotência/reversão verificadas na cópia. Três testes afetados/30 asserções, TypeScript, lint, PHPStan e build passaram. [Pré-visualização e prova nova](ativacao-rastreabilidade-territorial.md): válida até 10/10 às 02:13:54 locais. **Não reaplicado na base ativa; ação autenticada permanece por executar.** diff --git a/resources/js/components/territorial-traceability-plan.tsx b/resources/js/components/territorial-traceability-plan.tsx index 9466e314..4b64d9f5 100644 --- a/resources/js/components/territorial-traceability-plan.tsx +++ b/resources/js/components/territorial-traceability-plan.tsx @@ -5,6 +5,14 @@ export type TraceabilityPlan = { code_matches: boolean; backup_ready: boolean; applied: boolean; + reversed: boolean; + history: { + id: string; + action: string; + actor_user_id: number; + occurred_at: string; + reason: string | null; + }[]; plan: { target: { database: string; port: number }; rows: { @@ -42,6 +50,8 @@ export default function TerritorialTraceabilityPlan({ hash: preview.hash, operation: 'traceability', reverse: false, + reason: '', + confirm_reverse: false, }); return (
@@ -68,8 +78,23 @@ export default function TerritorialTraceabilityPlan({ {preview.code_matches ? 'corresponde ao plano' : 'plano deve ser atualizado'}{' '} - · Aplicação: {preview.applied ? 'aplicada' : 'não aplicada'} + · Aplicação:{' '} + {preview.applied + ? 'aplicada' + : preview.reversed + ? 'revertida; novo plano necessário' + : 'não aplicada'}

+
+ Histórico de aplicação e reversão + {preview.history.map((event) => ( +

+ {event.occurred_at} · {event.action} · conta{' '} + {event.actor_user_id} · Motivo:{' '} + {event.reason ?? 'não registado no evento original'} +

+ ))} +
{ e.preventDefault(); @@ -77,17 +102,55 @@ export default function TerritorialTraceabilityPlan({ hash: preview.hash, operation: 'traceability', reverse: preview.applied, + reason: form.data.reason, + confirm_reverse: form.data.confirm_reverse, })); form.post('/management/relationship-repair', { preserveScroll: true, }); }} > + {preview.applied && ( +
+ + Reversão — restitui as referências anteriores + +