Files
Investidor-V2/docs/investmoz/simulations/simulation-draft-continuity-incident-2026-10-04.md
T
nala e65cb26901
tests / ci (push) Canceled after 0s
varias alteracoes
2026-10-06 14:50:12 +02:00

6.2 KiB

Correcção da continuidade dos rascunhos — 2026-10-04

Causa confirmada

A aplicação e SimulationFormDraft já usavam expires_at em gravação, recuperação e expiração. A migration 2026_10_04_120000_add_simulation_draft_continuity estava pendente na base activa investemoz; a tabela existente tinha 12 colunas e não tinha expires_at nem claimed_token_hash. A limpeza selectiva de rascunhos expirados era executada pelo código novo sobre esse esquema antigo, causando SQLSTATE[42703]. Não era necessário reconstruir a base.

Dados antigos e prazo

Antes da migration havia 14 registos: 13 rascunhos anónimos activos e um associado a conta. Não existe last_activity_at nem evento de actividade com semântica fiável. O antigo GET /simulation-drafts/current alterava expires_at e, por consequência, updated_at; por isso, não se pode interpretar o updated_at histórico como actividade humana.

A migration pendente atribui aos rascunhos anónimos sem prazo GREATEST(created_at + 30 dias, instante da transição + 30 dias). Esta é a regra de transição segura: preserva todos os rascunhos e garante-lhes pelo menos 30 dias desde a correcção. Os 13 registos existentes receberam prazo futuro, com menor data observada em 2026-11-03 14:49:50 UTC. O dossier de conta mantém expires_at = NULL.

Depois da transição, updated_at reflecte escritas explícitas; visitas e leituras não alteram prazo nem actividade. Renovam o prazo anónimo por 30 dias: criar o rascunho, gravar conteúdo, anexar uma simulação e usar “Guardar e continuar mais tarde” (endpoint touch, com If-Match). Recuperar, abrir, navegar ou fazer polling não renova. O cookie HttpOnly, SameSite=Lax e limitado a /api/v1 só é emitido inicialmente, renovado numa escrita efectiva ou expirado na acção de esquecimento. O prazo do cookie e o do servidor são iguais, com tolerância inferior a um minuto na serialização HTTP.

Alterações de aplicação

  • EnsureSimulationRecoveryCookie: não renova em leituras; renova em acções explícitas; suporta o touch e replay idempotente sem prolongar o prazo do servidor.
  • SimulationFormDrafts e SimulationFormDraftController: leitura sem efeito lateral, renovação explícita com controlo If-Match, e esquecimento limitado ao rascunho anónimo do token actual.
  • routes/api.php: POST /simulation-drafts/{id}/touch e DELETE /simulation-drafts/current.
  • simulation-form.tsx: texto correcto sobre cookie persistente, botão acessível “Esquecer neste dispositivo” e renovação ao escolher “Guardar e continuar mais tarde”.
  • bootstrap/app.php: erros 5xx desta API devolvem mensagem genérica e referência de correlação; o relatório detalhado continua no log interno do Laravel.
  • openapi.json e SimulationDraftContinuityTest.php: contrato e testes dos novos comportamentos.

Migrações e backup

Backup PostgreSQL em formato custom, criado antes do DDL: %LOCALAPPDATA%\InvestMoz\backups\investmoz-before-simulation-draft-continuity-20261004.dump (SHA-256 20D940CAF3EAF3216CEFE45ED86B6C5AC593ECFE72C89253470711F2E32F0086). pg_restore --list validou o arquivo e a restauração foi ensaiada numa base separada: os mesmos 14 rascunhos foram lidos, e a base temporária foi removida.

Foram aplicadas individualmente apenas as migrations relacionadas com rascunhos:

  • 2026_10_02_120000_allow_owner_to_read_soft_deleted_simulation_drafts — corrige o predicado RLS de leitura para manter disponível a leitura autorizada do histórico eliminado.
  • 2026_10_04_120000_add_simulation_draft_continuity — adiciona os prazos e hashes de associação, permite vários dossiers por conta e cria a tabela de associação reversível a negócios.

Não foi executada limpeza em massa. As outras migrations pendentes não eram dependências desta correcção e ficaram pendentes: integridade de dependências de projectos, tipos de oportunidade de negócio existente, governação de regras fiscais, leitura pública de parâmetros fiscais, assistência fundiária e email opcional de manifestações.

Evidências

  • Testes PHP numa base PostgreSQL isolada: 10/10 testes, 78 asserções. Incluem gravação, renovação explícita, ausência de renovação por leitura, expiração, associação privada, esquecimento e resposta segura a erro SQL.
  • Contrato OpenAPI: 1/1 teste; JSON válido. npm.cmd run build terminou com sucesso (2403 módulos). O build assinalou o aviso pré-existente sobre o pacote opcional fontaine e um chunk JavaScript acima de 500 kB.
  • Base activa depois das migrations: expires_at e claimed_token_hash presentes, tabela de ligações presente, migrations registadas nos batches 29 e 30 e rascunhos anteriores preservados.
  • Edge em http://127.0.0.1:8000/Simulador, ligado à base investemoz: HTTP 200; rascunho sintético criado e recuperado depois de fechar e reabrir o browser; leitura não alterou validade do cookie; “Guardar e continuar mais tarde” renovou servidor e cookie com diferença inferior a um segundo; “Esquecer neste dispositivo” respondeu 204 e removeu o cookie. Sem erros JavaScript.
  • Relatório estruturado: evidência do browser e da continuidade activa. Capturas: antes de esquecer e depois de esquecer.

Na inspecção inicial havia 14 registos. No fim há 18: os três rascunhos explicitamente criados para os ensaios do browser ficaram soft-deleted pela própria acção de esquecimento. Um outro rascunho anónimo apareceu durante a primeira visita de teste; como não correspondeu aos títulos sintéticos identificados e não foi possível atribuí-lo com segurança, foi deixado intacto. O total activo verificado é 14 anónimos e um dossier de conta; nenhum dos 13 rascunhos que existiam antes da migration foi apagado.

Recuperação

Não executar o down() da migration: ele removeria colunas e a tabela de ligações. Se for indispensável recuperar o estado anterior, restaurar o arquivo para uma nova base, comparar escritas feitas após o backup e só então preparar uma troca controlada; não restaurar por cima da base activa.