fix: concluir recuperacao de exportacoes e consolidar candidata QA
tests / ci (push) Canceled after 0s
tests / ci (push) Canceled after 0s
This commit is contained in:
@@ -158,5 +158,166 @@
|
||||
4
|
||||
],
|
||||
"remaining_scope": "Read-only remote collection; native PDF; new cashflow visual continuation; evidence closure; excludes waiting and tax-engine integration",
|
||||
"secrets_and_private_fixtures": "Not included in source manifest or Git"
|
||||
"secrets_and_private_fixtures": "Not included in source manifest or Git",
|
||||
"current_reconciliation_20261021": {
|
||||
"items": {
|
||||
"AUD-SIM-INT01": {
|
||||
"status": "Concluído no recorte",
|
||||
"remaining": "Nenhum delta de validação de data por demonstrar; prova anterior preservada."
|
||||
},
|
||||
"AUD-SIM-INT02": {
|
||||
"status": "Parcial",
|
||||
"remaining": "Leitura remota concluída: zero regras de todos os estados na homologação, tal como QA; seis referências jurídicas não são parâmetros executáveis. Falta cadastro fundamentado por componente/âmbito/período e ligação funcional ao motor fiscal. Fontes e análises existentes reutilizáveis; aceitação oficial da Tempo de08/10 preservada. Não activar taxas nem importar/publicar por esta reconciliação."
|
||||
},
|
||||
"AUD-SIM-INT03": {
|
||||
"status": "Concluído no recorte",
|
||||
"remaining": "A/B, logout/novo login, recuperação e recusa pela aplicação demonstrados anteriormente. A tesouraria tem prova API de nova sessão e prova visual de recuperação por recarga. Não atribuir ao navegador nova sessão dos valores de tesouraria, nem nova certificação RLS; limites expressos conservados."
|
||||
},
|
||||
"AUD-SIM-INT04": {
|
||||
"status": "Concluído no recorte demonstrado",
|
||||
"remaining": "Histórico24/12 visível, hashes preservados e versões temporais sintéticas do resolvedor conferidas. Execução fiscal integrada com parâmetros reais pertence ao saldo de02 e não foi demonstrada por estes testes."
|
||||
},
|
||||
"AUD-SIM-INT05": {
|
||||
"status": "Concluído nos controlos delimitados",
|
||||
"remaining": "Controlos independentes de caixa/plano/crédito, zero/desconhecido e moedas preservados. Não são validação de taxas legais nem certificação de toda a contabilidade."
|
||||
},
|
||||
"AUD-SIM-INT06": {
|
||||
"status": "Concluído na apresentação e comparação deste relatório",
|
||||
"remaining": "Rodapé do PDF nativo fechado em QA segundo provas apresentadas;10 páginas/valores/CSV não repetidos. Tempo declarou que não abriu imagem/PDF pelos caminhos locais: registar aceitação das provas, não revisão visual directa do utilizador. Outras provas de relatório conservadas; falhas interactivas de exportação/anexos mantêm saldo em07."
|
||||
},
|
||||
"AUD-SIM-INT07": {
|
||||
"status": "Parcial",
|
||||
"remaining": "Gravação503→nova tentativa→recuperação e exportações normais demonstradas. Faltam falhas dirigidas de geração Word/ZIP/PDF e retoma; teste interactivo de limite de anexo não ocorreu por bloqueio da ferramenta antes do envio. Nova leitura do código: três catch apresentam cause.message para Error, podendo mostrar texto técnico sem tradução. Sem nova falha de execução alegada."
|
||||
},
|
||||
"AUD-SIM-INT08": {
|
||||
"status": "Parcial",
|
||||
"remaining": "Inventário estático,404 real e traduções de validação preservados. Classificar/apresentar em português os erros dinâmicos das bibliotecas de exportação (cause.message, três consumidores) e conferir estados residuais/governação delimitados; não certificar todos os textos dinâmicos do catálogo."
|
||||
}
|
||||
},
|
||||
"remaining_hours_delimited": [
|
||||
1,
|
||||
2
|
||||
],
|
||||
"integral_fiscal_implementation_estimated": false,
|
||||
"user_direct_visual_review_of_local_files": false,
|
||||
"native_footer_accepted_on_presented_evidence": true,
|
||||
"remote_rule_count": 0,
|
||||
"remote_legal_reference_count": 6,
|
||||
"supersedes_prior_pending": [
|
||||
"remote authentication and inventory",
|
||||
"native print save and visual footer correction",
|
||||
"cashflow screen/download values"
|
||||
],
|
||||
"tool_limits_retained": [
|
||||
"interactive file chooser before attachment upload",
|
||||
"native save performed by user"
|
||||
],
|
||||
"branch": "codex/simulator-integrated-review",
|
||||
"main_merge": false,
|
||||
"read_only_evidence_sha256": "bbb2d2f0b001f1f439ce2bf8e60a4233467463f5cfa430dd05b82f515f5d9a5f"
|
||||
},
|
||||
"candidate_20261021": {
|
||||
"branch": "codex/simulator-integrated-review",
|
||||
"base": "c22003a3",
|
||||
"main_integration": false,
|
||||
"active_changes": false,
|
||||
"new_tests": 4,
|
||||
"typescript": "passed",
|
||||
"browser": {
|
||||
"word_failure_retry": "passed, 2 attempts / 1 success",
|
||||
"pdf_failure_retry": "passed, failed popup closed / one populated retry",
|
||||
"zip_failure_retry": "passed: failed ZIP, edited title/unchecked section and one attachment retained; retry produced one valid archive",
|
||||
"attachment_limit_ui": "passed: refused16MB, prior valid entry retained",
|
||||
"attachment_valid_retry": "passed: duplicate selection leaves one entry and translated status"
|
||||
},
|
||||
"duplicate_prevention": "synchronous gate test passed; attachment name+SHA256 identity test passed; ZIP single original test passed",
|
||||
"logs": "local diagnostic console; no backend telemetry added",
|
||||
"accepted_evidence_reused": [
|
||||
"native PDF 10-page footer QA acceptance",
|
||||
"financial values and CSV",
|
||||
"A/B and new session",
|
||||
"historical snapshots and temporal resolver",
|
||||
"read-only remote fiscal inventory"
|
||||
],
|
||||
"limitations": [
|
||||
"Synthetic wrapper injects generator faults only and is excluded from deployment",
|
||||
"Native print and values/CSV proofs reused, not repeated",
|
||||
"Transient attachment read/concurrent-click proofs are automated; manual chooser path observed separately",
|
||||
"Local console diagnostics are not persistent server audit",
|
||||
"Fiscal integration remains incomplete; catalogue/governance language is not globally certified"
|
||||
],
|
||||
"sources": [
|
||||
{
|
||||
"path": "resources/js/components/project-dossier-export.tsx",
|
||||
"sha256": "c0bbea29c8e307785cbe54014a85d6271c723a541bea9f2ad5196118b9570764"
|
||||
},
|
||||
{
|
||||
"path": "resources/js/lib/dossier-export-state.ts",
|
||||
"sha256": "ac40e30134ba56b67c6c6a3b044aaeb110750cac53e0934794c8a7d346d745fe"
|
||||
},
|
||||
{
|
||||
"path": "resources/js/lib/project-dossier-document.ts",
|
||||
"sha256": "3bec7565324587bdd42f65f5b3c723597ec554b1c80376bfb45d350516eae16f"
|
||||
},
|
||||
{
|
||||
"path": "tests/frontend/dossier-export-recovery.test.mjs",
|
||||
"sha256": "b4112d612ce204c41b7376e64a0c15e7b85a6ec19087b0ed5415c1114d99f2b2"
|
||||
},
|
||||
{
|
||||
"path": "tests/frontend/project-dossier-report.test.mjs",
|
||||
"sha256": "77e33425e33e7ca0290e60e0b09fab7bbe5c8bd1eef196dd0863452da8f06271"
|
||||
}
|
||||
],
|
||||
"remaining_engineering_hours_delimited": [
|
||||
0,
|
||||
0
|
||||
],
|
||||
"review_pending": true,
|
||||
"build_manifest_sha256": "e4846e02db6112bce9c1f5466038e0a25f381c17fdcd8fe54248a7068a0645dd",
|
||||
"evidence": [
|
||||
{
|
||||
"path": "outputs\\simulator-continuation-20261020\\export-word-error-20261021.txt",
|
||||
"sha256": "b1c984663ea27bbeb5bfe760ab181314142f32cc9f6b27fa11c2fb7d7f0456ca"
|
||||
},
|
||||
{
|
||||
"path": "outputs\\simulator-continuation-20261020\\export-pdf-error-20261021.txt",
|
||||
"sha256": "b46f597e185874875d823d2c90219b555ee1f2028b5d144e00754db73468ed41"
|
||||
},
|
||||
{
|
||||
"path": "outputs\\simulator-continuation-20261020\\export-pdf-retry-20261021.txt",
|
||||
"sha256": "091679de9a60e4f22e2d0cb2127b736addfc9e2752fb0bb2aec051ddcb51f3c8"
|
||||
},
|
||||
{
|
||||
"path": "outputs\\simulator-continuation-20261020\\export-zip-error-20261021.txt",
|
||||
"sha256": "eda788b6f69b7fbcbb13aadf44dd0d3fbe695a2f050445b6f6a6d6553e7dc63d"
|
||||
},
|
||||
{
|
||||
"path": "outputs\\simulator-continuation-20261020\\export-zip-retry-20261021.txt",
|
||||
"sha256": "ef8c3c2197cb77260c1f8a1f169f282f1a14376e2aa331d26e9b98aee45e92c3"
|
||||
},
|
||||
{
|
||||
"path": "outputs\\simulator-continuation-20261020\\attachment-limit-preserved-20261021.txt",
|
||||
"sha256": "2132c4ddfaf044e8349766322514e9c6ce5c1a0f06236bfb4323b8a010e73197"
|
||||
},
|
||||
{
|
||||
"path": "outputs\\simulator-continuation-20261020\\attachment-duplicate-retry-20261021.txt",
|
||||
"sha256": "3027ea5949168df879442bb04a481d3f8c5e4741e470d30875e1dc6c25892312"
|
||||
},
|
||||
{
|
||||
"path": "outputs\\simulator-continuation-20261020\\export-zip-content-proof-20261021.json",
|
||||
"sha256": "6ee6bece4d2110c996bad2d686b24970c9d199eff2940dfba34e285f005f0f7c"
|
||||
}
|
||||
],
|
||||
"build_exit_code": 0,
|
||||
"existing_items_current_scope": {
|
||||
"AUD-SIM-INT01": "demonstrated within bounded scope; see existing manual for limits",
|
||||
"AUD-SIM-INT02": "partial; fiscal implementation remains open",
|
||||
"AUD-SIM-INT03": "demonstrated within bounded scope; see existing manual for limits",
|
||||
"AUD-SIM-INT04": "demonstrated within bounded scope; see existing manual for limits",
|
||||
"AUD-SIM-INT05": "demonstrated within bounded scope; see existing manual for limits",
|
||||
"AUD-SIM-INT06": "demonstrated within bounded scope; see existing manual for limits",
|
||||
"AUD-SIM-INT07": "demonstrated within bounded scope; see existing manual for limits",
|
||||
"AUD-SIM-INT08": "demonstrated within bounded scope; see existing manual for limits"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1623,3 +1623,94 @@ Artefactos em `outputs/simulator-continuation-20261020/`: `interface-comparison.
|
||||
Provas adicionais: teste HTTP com dois dossiers, logout lógico/nova sessão, recuperação exacta e recusa por outra conta passou (8 asserções), em transacção revertida no clone. Quatro controlos frontend dirigidos passaram, incluindo recuperação de mês vazio, valor inválido para correcção, relatório sem zero inventado e pressupostos completos do crédito. As 10 verificações do exportador afectado passaram; teste temporal sintético passou (7 asserções); erro HTTP em português passou (3). TypeScript/build e sintaxe PHP passaram. A conferência visual do novo componente de tesouraria com sessão real QA permanece por executar depois de fechar a impressão do Edge; não foi substituída por uma prova unitária.
|
||||
|
||||
**Saldo revisto após esse achado: 2–4 h de verificação delimitada**, já descontado o desenvolvimento concluído: leitura remota 0,5–1 h; PDF nativo 0,5–1 h; continuidade visual/saída da tesouraria recém-corrigida 0,5–1 h; consolidação das provas e limitações 0,5–1 h. Exclui espera por autenticação/ficheiro e a integração funcional futura das regras fiscais. Esta estimativa substitui 1–3 h acima por uma divergência concreta; não retoma as 6–10 h nem as 18–30 h originais.
|
||||
|
||||
|
||||
### Retoma da conferência visual de tesouraria — 21/10/2026
|
||||
|
||||
[Retoma 21/10/2026] QA 18576, clone simulator_review_delta_20261020_test: sessão sintética renovada por login normal; gravação autenticada e recarga recuperaram saldo 123, receita 900, custos 600, calendário mensal 10/vazio/0, crédito IVA estimado 500. Caixa conhecida 3713 MZN; total indeterminado desde mês 2; crédito estimado excluído da caixa. Prova visual cashflow-recovered-20261021.png e snapshot .txt em outputs/simulator-continuation-20261020. Não certifica recuperação destes novos valores após outro login, CSV/DOCX ou PDF nativo. Ligação de controlo do navegador perdida durante espera de download; não atribuir falha à aplicação. Leitura remota aguarda identificação da ligação SSH; zero alterações no destino.
|
||||
|
||||
|
||||
### Leitura fiscal remota e saída nativa — 21/10/2026
|
||||
|
||||
[Leitura SSH 21/10/2026 08:17 Maputo] Zero regras em simulation_tax_rule_versions na homologação, incluindo todos os estados por leitura administrativa sem ocultação RLS. Seis referências em investment_legal_sources; não são parâmetros executáveis. QA também tinha zero regras. BEGIN READ ONLY/SELECT/ROLLBACK; zero escritas. Prova homologation-fiscal-readonly.json. O git HEAD registado é do checkout consultado, não certifica a revisão do build servido. A ausência de cadastro não é ausência de fontes oficiais nem reabre a aceitação da Tempo de 08/10. Não implementa a ligação funcional ao motor fiscal.
|
||||
|
||||
[Exportação nativa 21/10/2026] Saída actual aberta pelo botão do portal: revisão 4, 12 meses, MZN, saldo inicial123/receita900/custos600, calendário10/vazio/0, caixa conhecida3713 e total indeterminado coincidem com o ecrã. Crédito estimado500 identificado como pressuposto, não somado à caixa. Prova portal-print-current-20261021.txt. Relatório de20/10/revisão2 preservado, sem o confundir com o actual. Downloads: sem novo CSV na pasta habitual ou nos temporários locais conferidos; isso não demonstra falha nem cancelamento. Histórico de downloads bloqueado pela política da ferramenta; caminho do ficheiro solicitado antes de repetir. PDF nativo aberto, mas paginação/legibilidade aguardam o ficheiro efectivamente guardado pelo Edge; conversão DOCX/PDF anterior não conta como essa prova. Perda da ligação e bloqueio de controlo durante impressão são limitações da ferramenta, sem falha do simulador demonstrada.
|
||||
|
||||
|
||||
### Ficheiros de Transferências recuperados — 21/10/2026
|
||||
|
||||
[Fecho da verificação dos ficheiros 21/10/2026] Transferências Windows redireccionada para \\Mac\Home\Downloads; CSV concluído08:12:46, PDF nativo08:17:19. Sem repetir downloads. Os12 registos mensais do CSV coincidem exactamente com o snapshot do ecrã após correspondência de colunas; PDF revisão4 conserva valores mensais, MZN,12meses,123/900/600, crédito estimado500 não caixa e caixa conhecida3713. Os10 PNG do PDF nativo foram inspeccionados: defeito confirmado na linha repetida de rodapé, sobreposta ao corpo perto do topo de páginas, incluindo2,6e10. Conteúdo/valores conferidos; paginação/legibilidade não aceites até correcção e nova prova nativa. Não é conversão DOCX/PDF. Perda de ligação continua limitação da ferramenta e não causou falha do download; sobreposição é defeito distinto demonstrado no PDF. Provas: download-native-inspection-20261021.json, portal-cashflow-20261021.csv, portal-native-20261021.pdf e native-pdf-render/. A indicação anterior de ficheiros pendentes fica superada por esta prova. Sem alteração funcional nem escrita remota.
|
||||
|
||||
|
||||
### Correcção do rodapé do PDF nativo em QA — 21/10/2026
|
||||
|
||||
[Correcção do rodapé nativo QA 21/10/2026] Origem: pedido expresso da Tempo nesta conversa; solução de implementação, sem regra organizacional nova. Causa em createProjectDossierPrintHtml (project-dossier-document.ts): footer position:fixed/bottom:-14mm sobrepunha a área fragmentada de impressão. Substituído na impressão por caixas @page @bottom-left/@bottom-right, dentro da margem inferior reservada20mm, com identificação/revisão e número de página/total. Rodapé normal de ecrã conservado; sem diminuir letras do corpo (10,5pt), tabelas, cabeçalhos ou origens (9pt), nem retirar conteúdo. Mesmo dossier B/revisão4 regenerado pelo botão do portal QA18576 e guardado no Edge, não por conversão DOCX. 10→10 páginas A4; comparação automática do corpo por página, descontando apenas hora de emissão/rodapé/contador, coincide integralmente. Inspecção visual dos10 PNG: sem sobreposição, cabeçalhos de tabela repetidos, quebras/linhas preservadas, última página íntegra e numeração1/10→10/10. Rodapés medidos inteiramente na margem inferior. Ecrã QA conferido após build; estilos screen e geradores DOCX/ZIP inalterados por comparação de fontes. 10 testes do exportador, TypeScript e build passaram; escape da revisão na nova string CSS verificado. Reutilizadas provas de valores e CSV aceites; não repetido download/aritmética. Provas: native-footer-fix-20261021.json; portal-native-fixed-20261021.pdf; native-footer-before-after-20261021.jpg; native-pdf-fixed-render/; screen-after-print-fix-20261021.png. Fechado exclusivamente o defeito de rodapé neste relatório QA; limitações e restantes critérios de AUD-SIM-INT06 mantêm âmbito próprio. Relato anterior de sobreposição fica superado neste delta pela comparação antes/depois. Branch codex/simulator-integrated-review separada de main, sem integração, escrita remota ou disponibilização activa; revisão prevista mantém-se pendente.
|
||||
|
||||
|
||||
### Saldo consolidado dos oito itens — 21/10/2026
|
||||
|
||||
Reconciliação a pedido expresso da Tempo. O fecho do rodapé limita-se à apresentação deste relatório e baseia-se nas provas apresentadas; o utilizador declarou não ter conferido a imagem nem o PDF pelos caminhos locais. Não é fecho da revisão integral do simulador. Reutilizadas as provas aceites, sem repetir10 páginas, valores,CSV ou consulta SSH.
|
||||
|
||||
| Item existente | Estado no recorte | Saldo / limite |
|
||||
|---|---|---|
|
||||
| AUD-SIM-INT01 | Concluído no recorte | Nenhum delta de validação de data por demonstrar; prova anterior preservada. |
|
||||
| AUD-SIM-INT02 | Parcial | Leitura remota concluída: zero regras de todos os estados na homologação, tal como QA; seis referências jurídicas não são parâmetros executáveis. Falta cadastro fundamentado por componente/âmbito/período e ligação funcional ao motor fiscal. Fontes e análises existentes reutilizáveis; aceitação oficial da Tempo de08/10 preservada. Não activar taxas nem importar/publicar por esta reconciliação. |
|
||||
| AUD-SIM-INT03 | Concluído no recorte | A/B, logout/novo login, recuperação e recusa pela aplicação demonstrados anteriormente. A tesouraria tem prova API de nova sessão e prova visual de recuperação por recarga. Não atribuir ao navegador nova sessão dos valores de tesouraria, nem nova certificação RLS; limites expressos conservados. |
|
||||
| AUD-SIM-INT04 | Concluído no recorte demonstrado | Histórico24/12 visível, hashes preservados e versões temporais sintéticas do resolvedor conferidas. Execução fiscal integrada com parâmetros reais pertence ao saldo de02 e não foi demonstrada por estes testes. |
|
||||
| AUD-SIM-INT05 | Concluído nos controlos delimitados | Controlos independentes de caixa/plano/crédito, zero/desconhecido e moedas preservados. Não são validação de taxas legais nem certificação de toda a contabilidade. |
|
||||
| AUD-SIM-INT06 | Concluído na apresentação e comparação deste relatório | Rodapé do PDF nativo fechado em QA segundo provas apresentadas;10 páginas/valores/CSV não repetidos. Tempo declarou que não abriu imagem/PDF pelos caminhos locais: registar aceitação das provas, não revisão visual directa do utilizador. Outras provas de relatório conservadas; falhas interactivas de exportação/anexos mantêm saldo em07. |
|
||||
| AUD-SIM-INT07 | Parcial | Gravação503→nova tentativa→recuperação e exportações normais demonstradas. Faltam falhas dirigidas de geração Word/ZIP/PDF e retoma; teste interactivo de limite de anexo não ocorreu por bloqueio da ferramenta antes do envio. Nova leitura do código: três catch apresentam cause.message para Error, podendo mostrar texto técnico sem tradução. Sem nova falha de execução alegada. |
|
||||
| AUD-SIM-INT08 | Parcial | Inventário estático,404 real e traduções de validação preservados. Classificar/apresentar em português os erros dinâmicos das bibliotecas de exportação (cause.message, três consumidores) e conferir estados residuais/governação delimitados; não certificar todos os textos dinâmicos do catálogo. |
|
||||
|
||||
**Estimativa restante deste recorte:1–2h de engenharia**, excluindo espera por interacção necessária da ferramenta: 07/08 agrupados0,75–1,5h para tratamento e conferência dirigida de erros dinâmicos/retoma/anexos; 0,25–0,5h para reconciliação final das provas. Não somar o mesmo erro duas vezes por aparecer em07e08. Substitui o saldo anterior2–4h porque leitura remota, apresentação nativa e comparação da tesouraria já foram executadas. Não inclui cadastrar/aplicar regras fiscais reais, integrar fórmulas no motor ou certificar todo o catálogo. O esforço para02 integral permanece por dimensionar por componente, âmbito, período e correspondência documental; não há estimativa de conclusão integral obtida apenas deste1–2h.
|
||||
|
||||
Próxima execução delimitada: corrigir a apresentação de excepções brutas nos três exportadores, conferir falha/retoma e os limites interactivos que faltam, preservando os relatórios/CSV já aceites. Branch codex/simulator-integrated-review mantida separada de main até à revisão consolidada. Nenhuma nova concessão, regra fiscal, migration ou escrita remota.
|
||||
|
||||
|
||||
### Candidata consolidada da revisão do simulador — 21/10/2026
|
||||
|
||||
Origem: pedido expresso da Tempo para fechar apenas falhas dirigidas de exportação/anexos, apresentação das mensagens e consolidação. As 1–2 h anteriores respeitavam a esse recorte, não à implementação fiscal integral. A branch `codex/simulator-integrated-review` permanece separada de `main`; não integrar antes da revisão consolidada porque a cadeia existente disponibiliza automaticamente a revisão integrada.
|
||||
|
||||
**Separação funcional preservada.** Os cálculos de planeamento usam valores declarados pelo utilizador, com origem e pressupostos identificados. O relatório não os transforma em resultados fiscais. Falta de imposto/parâmetro continua indeterminada, não zero; crédito/pedido/reconhecimento de IVA não se torna dinheiro recebido. A correcção de falhas de exportação e do rodapé não encerra a componente fiscal.
|
||||
|
||||
**Implementação deste delta.** O exportador apresenta mensagens em português, identifica a operação e a referência de diagnóstico e mantém as selecções para nova tentativa pelo mesmo botão. A excepção original é registada na consola técnica local com operação/referência; não há novo envio ao servidor, telemetria nem registo de conteúdo do dossier/anexos. A consola é um diagnóstico local, não uma auditoria persistente no servidor. Uma guarda síncrona recusa outra execução enquanto a primeira está activa, mesmo antes da actualização do estado React; após erro a guarda é libertada. Preparação dos anexos é atómica: falhas/limites não substituem a lista existente nem as legendas. A selecção repetida do mesmo nome com os mesmos bytes mantém uma entrada; bytes diferentes com o mesmo nome e nomes diferentes continuam distintos. Esta identidade é escolha de implementação local, não regra de cadastro/publicação documental. Os limites de quantidade/tamanho já existentes não foram alterados.
|
||||
|
||||
**Provas reutilizadas.** Data inválida; A/B e nova sessão; recusa de outra conta; histórico24/12 e hashes; resolvedor temporal sintético; controlos financeiros independentes; Word/CSV e respectivos valores; PDF nativo10 páginas/rodapé/última página; inventários/documentos e leitura SSH sem escrita. A Tempo aceitou o fecho do rodapé segundo as provas apresentadas e declarou que não abriu a imagem/PDF pelos caminhos locais. Não lhe atribuir inspecção visual directa. Os testes temporais sintéticos não representam legislação. A prova de recusa do clone com superutilizador é da aplicação; não é uma nova certificação de RLS. Não repetir as provas aceites apenas para gerar uma nova lista de resultados.
|
||||
|
||||
**Ensaio dirigido isolado.** `http://127.0.0.1:18576/qa/export-recovery` usa o componente real e os geradores reais, com uma falha única de biblioteca injectada no módulo de ensaio, sem alteração das rotas de produção. Os controlos de falha e os ficheiros sintéticos ficam em `outputs/`, fora do Git e do pacote. Os contadores referem-se a chamadas/generações; para ZIP contam a geração interna de Word e a preparação do arquivo, não duas transferências. No ensaio de nova tentativa de PDF é omitida apenas a impressão automática, para observar a janela com conteúdo e não repetir a prova nativa aceite. Nenhuma dessas injecções entra na candidata funcional.
|
||||
|
||||
**Procedimento de integração após revisão.** Fixar o commit candidato e manifesto de fontes; obter o `main` remoto actualizado e reconciliar apenas os deltas dos programadores que afectem estas fontes/consumidores. Rever o diff e conferir no ambiente QA a revisão exacta a integrar; repetir apenas verificações afectadas por conflitos ou alterações posteriores. Este delta não contém migrations, cadastro de regras, importação, publicação de documentos, contas, permissões ou alterações de MFA/configuração. Após decisão da Tempo, integrar pelo processo Git existente; confirmar o recibo da cadeia e a revisão efectivamente servida em `/build/revision.json`, com consulta pública e login privado conferidos nos consumidores afectados. Uma falha deve conservar a release anterior; não editar a pasta servida para contornar a cadeia. A reversão usa o mecanismo de release anterior de código/build, preservando base, anexos, configuração e dados posteriores; não repor um dump antigo. Se uma futura entrega incluir regras/dados activos, exigir o procedimento específico já aprovado de backup/restauro e acção autenticada; esse trabalho não está incluído aqui.
|
||||
|
||||
### Diagnóstico fiscal separado da candidata — 21/10/2026
|
||||
|
||||
| Camada | Existente e comprovado | Saldo efectivo |
|
||||
|---|---|---|
|
||||
| Fontes | Inventário anterior de9901 caminhos e32 candidatos por nome; três originais versionados: BR248/I/Suplemento1/2025, republicação Lei22/2022 no BR250/2.ºSuplemento e BR89/2017. Análises em `primary-text-review-2025-2026.md`, `official-source-register.md`, `fiscal-reform-2026-review.md` e referências em `fiscal-source-references-2026-10-06.csv`. | Reutilizar conteúdo, hashes, versões e citações já disponíveis; não pedir novamente esses documentos. A enumeração de caminhos não equivale à leitura de todos os binários. Distinguir fontes já lidas de fontes só inventariadas e alterações/anexos ainda sem correspondência. |
|
||||
| Parâmetros executáveis | Leitura administrativa de todos os estados confirmou zero linhas em `simulation_tax_rule_versions` tanto na QA como na homologação. A homologação tem seis referências em `investment_legal_sources`, ligadas a estruturas territoriais; não são taxas/fórmulas prontas. | Cadastrar por componente: código/versão, diploma/artigo/versão/hash, âmbito e condições, início/fim de aplicação, valor/unidade, bases/limites/exclusões, fórmula e arredondamento fundamentados. Os campos necessários variam por regra; não preencher lacunas por zero ou pela taxa mais recente. |
|
||||
| Governação e consulta | Modelo versionado, controlador de transições, API de resolução por componente/período/contexto, selecção de publicados e compatibilidade de versão da fonte; resolução temporal e indeterminação já ensaiadas. | Reutilizar estes serviços; conferir as correspondências documentais e a operação com parâmetros reais quando estes existirem. Não criar aprovações, nomeações ou uma nova exigência organizacional por escolha técnica. A Tempo define os papéis e as regras. |
|
||||
| Expressões | `SimulationTaxRuleExpression` já valida e avalia a DSL `investmoz.tax.v1` com aritmética racional, arredondamento e valores em falta. | Não reconstruir o avaliador. Verificar se cada regra fundamentada cabe na linguagem existente e estender apenas operações concretamente necessárias. |
|
||||
| Execução fiscal integrada | `RunSimulation` executa `base × taxa/100 × (1−redução/100)`, identifica `custom_planning`, `legal_interpretation=false`, `ruleset_version=null` e moeda não indicada nesse cenário. Consulta/DSL não são consumidas por este fluxo como motor fiscal. | Desenvolver orquestração explícita de componente/período/âmbito → regras elegíveis → entradas e parâmetros → avaliação → resultado e explicação com fontes/versões imutáveis. Tratar ambiguidade/transição/ausência como indeterminação. Manter o cenário de planeamento separado, sem o relabelar como imposto calculado. |
|
||||
| Resultado, dossier e relatório | Snapshots, histórico e exportadores existentes podem transportar resultados e proveniência; relatórios actuais identificam pressupostos/estimativas. | Integrar o resultado fiscal com identificação própria de regra, período, unidade/moeda, base, passos, arredondamento e lacunas. Guardar reprodução histórica e demonstrar casos de referência por regra, isolamento e não reaplicação de uma versão actual a um resultado antigo. |
|
||||
|
||||
**Componentes a parametrizar, não taxas inventadas.** O acervo já analisado identifica famílias de alterações para ICE (Lei7/2025/tabela), ISPC (Lei9/2025), IVA (republicação Lei22/2022 e Lei10/2025, além da consolidação/rectificação aplicável), IRPS (Lei11/2025) e IRPC (Lei12/2025). A API também contempla INSS, retenções e direitos aduaneiros. Isto é inventário de domínios, não prova de cobertura documental completa nem autorização para aplicar uma taxa genérica. Para ICE/aduaneiro são relevantes código/unidade/tabela e período; para IVA, operação, incidência/exclusões/dedução e estado de reembolso; para ISPC, elegibilidade/limites e obrigações; para IRPS/IRPC, categoria, residência/estabelecimento, base, retenções e regimes específicos. Prazos, taxas, anexos e incentivos só entram com correspondência documental aplicável.
|
||||
|
||||
A aceitação das fontes oficiais determinada pela Tempo em08/10 prevalece sobre pedidos históricos genéricos de reconfirmação de autenticidade ou de um fiscalista adicional. Identificar o artigo, âmbito, período, alterações/revogação e parâmetros é aplicação/rastreabilidade da fonte aceite, não segunda validação da sua credibilidade. A biblioteca documental, a situação jurídica e a utilização em cálculo são estados independentes; importar ou publicar documentos nunca activa cálculo fiscal.
|
||||
|
||||
**Desenvolvimento fiscal necessário:** correspondência documental e cadastro versionado; adaptação concreta de expressões quando necessária; orquestração do cálculo; contrato de resultado/proveniência/histórico; ligação à interface/relatórios e casos de referência por componente/período. Nenhuma dessas frentes é encerrada por corrigir crédito, orçamento, tesouraria, falhas de exportação ou rodapé. O esforço integral deve ser dimensionado por regras/componentes e lacunas documentais identificadas; não cabe na estimativa de1–2h do recorte de exportação.
|
||||
|
||||
### Fecho do recorte de exportações e saldo da candidata — 21/10/2026
|
||||
|
||||
| Item existente | Saldo consolidado da candidata | Prova / limite |
|
||||
|---|---|---|
|
||||
| AUD-SIM-INT01 | Concluído no recorte | Validação de data: prova anterior reutilizada. |
|
||||
| AUD-SIM-INT02 | Parcial; implementação fiscal integral aberta | Zero regras remotas/QA; fontes, cadastro e integração discriminados no diagnóstico separado. |
|
||||
| AUD-SIM-INT03 | Concluído nas provas delimitadas | A/B/nova sessão e recusas anteriores; tesouraria com API/nova sessão e UI/recarga, sem alegar uma nova prova browser da mesma sessão. Limite RLS mantido. |
|
||||
| AUD-SIM-INT04 | Concluído no recorte demonstrado | Histórico/snapshots e resolvedor sintético reutilizados; integração real fiscal permanece em02. |
|
||||
| AUD-SIM-INT05 | Concluído nos controlos delimitados | Controlos independentes e provas financeiras aceites reutilizados; sem validação legal de impostos. |
|
||||
| AUD-SIM-INT06 | Concluído na apresentação/comparação deste relatório | Rodapé nativo aceito segundo provas; sem repetir10páginas/valores/CSV; sem atribuir inspecção local ao utilizador. |
|
||||
| AUD-SIM-INT07 | Concluído no recorte dirigido QA |3falhas de geração/retoma, anexo16MB recusado com lista conservada, re-selecção sem duplicação e ZIP íntegro; injecção sintética fora da entrega. |
|
||||
| AUD-SIM-INT08 | Concluído nos consumidores afectados | Mensagens portuguesas/diagnóstico técnico separado; limites de catálogo/governação mantidos, sem certificação global. |
|
||||
|
||||
Esforço restante do recorte de engenharia de exportações:0h; revisão consolidada e eventual integração continuam pendentes. As1–2h anteriores foram estimativa deste recorte; não eram estimativa da implementação fiscal integral. Não fechar a revisão integral do simulador nem AUD-SIM-INT02 por este incremento. Word/PDF/ZIP foram ensaiados no componente real com falhas sintéticas de geração de um invólucro QA privado; ficheiros de67bytes e16MiB seleccionados manualmente pelo utilizador, estados observados directamente pelo Codex. A primeira recusa encontrou lista vazia e não foi usada como prova de preservação; a sequência posterior demonstrou lista válida→recusa16MB mantendo1→re-selecção válida mantendo1. ZIP real verificado por bytes, entrada única, título e secção. Leitura transitória falhada e concorrência demonstradas nos testes dirigidos, não por avaria real do sistema de ficheiros. A limitação anterior da selecção automática não foi contornada: a selecção normal foi manual. Os detalhes técnicos permanecem em diagnóstico local; não foi criado serviço de recolha remoto.
|
||||
|
||||
Provas actuais em outputs/simulator-continuation-20261020/: export-word-error-20261021.*, export-pdf-error-20261021.txt, export-pdf-retry-20261021.txt, export-zip-error-20261021.*, export-zip-retry-20261021.txt, attachment-limit-preserved-20261021.*, attachment-duplicate-retry-20261021.*, export-zip-content-proof-20261021.json e export-recovery-candidate-20261021.json. O procedimento de integração e diagnóstico fiscal imediatamente anteriores integram esta mesma candidata e o mesmo registo; sem matriz/backlog adicional.
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -1,11 +1,15 @@
|
||||
import { useMemo, useState } from 'react';
|
||||
import { useMemo, useRef, useState } from 'react';
|
||||
import {
|
||||
DossierExportGate,
|
||||
dossierExportFailure,
|
||||
prepareDossierAttachments,
|
||||
} from '@/lib/dossier-export-state';
|
||||
import {
|
||||
createProjectDossierPrintHtml,
|
||||
createProjectDossierReport,
|
||||
dossierReportSections,
|
||||
generateProjectDossierDocx,
|
||||
generateProjectDossierPackage,
|
||||
validateDossierAttachments,
|
||||
type DossierAttachment,
|
||||
type DossierReference,
|
||||
type DossierCurrentCalculation,
|
||||
@@ -57,6 +61,8 @@ export function ProjectDossierExport({
|
||||
const [attachments, setAttachments] = useState<DossierAttachment[]>([]);
|
||||
const [error, setError] = useState('');
|
||||
const [busy, setBusy] = useState('');
|
||||
const [notice, setNotice] = useState('');
|
||||
const exportGate = useRef(new DossierExportGate());
|
||||
const selected = useMemo(
|
||||
() => attachments.filter((item) => item.include),
|
||||
[attachments],
|
||||
@@ -66,20 +72,33 @@ export function ProjectDossierExport({
|
||||
[selectedSections],
|
||||
);
|
||||
|
||||
function addFiles(files: FileList | null): void {
|
||||
async function addFiles(files: FileList | null): Promise<void> {
|
||||
if (!files?.length) return;
|
||||
const incoming = Array.from(files);
|
||||
const combined = [...attachments.map((item) => item.file), ...incoming];
|
||||
const problem = validateDossierAttachments(combined);
|
||||
if (problem) {
|
||||
setError(problem);
|
||||
return;
|
||||
}
|
||||
setAttachments((current) => [
|
||||
...current,
|
||||
...incoming.map((file) => ({ file, include: true, caption: '' })),
|
||||
]);
|
||||
setError('');
|
||||
await exportGate.current.run(async () => {
|
||||
setBusy('attachments');
|
||||
setNotice('');
|
||||
try {
|
||||
const result = await prepareDossierAttachments(
|
||||
attachments,
|
||||
incoming,
|
||||
);
|
||||
if (result.problem) {
|
||||
setError(result.problem);
|
||||
return;
|
||||
}
|
||||
setAttachments(result.attachments);
|
||||
setError('');
|
||||
if (result.duplicateCount)
|
||||
setNotice(
|
||||
'Os ficheiros que já estavam na lista foram mantidos uma única vez.',
|
||||
);
|
||||
} catch (cause) {
|
||||
setError(dossierExportFailure('attachments', cause));
|
||||
} finally {
|
||||
setBusy('');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function toggleSection(id: DossierReportSectionId, checked: boolean): void {
|
||||
@@ -118,99 +137,103 @@ export function ProjectDossierExport({
|
||||
}
|
||||
|
||||
async function exportWord(): Promise<void> {
|
||||
setBusy('word');
|
||||
setError('');
|
||||
try {
|
||||
const report = model();
|
||||
const blob = await generateProjectDossierDocx(
|
||||
report,
|
||||
selected,
|
||||
selectedSections.has('annexes'),
|
||||
);
|
||||
download(
|
||||
blob,
|
||||
`relatorio-investmoz-${filenameStem(report.title)}.docx`,
|
||||
);
|
||||
} catch (cause) {
|
||||
setError(
|
||||
cause instanceof Error
|
||||
? cause.message
|
||||
: 'Não foi possível gerar o relatório Word.',
|
||||
);
|
||||
} finally {
|
||||
setBusy('');
|
||||
}
|
||||
await exportGate.current.run(async () => {
|
||||
setBusy('word');
|
||||
setError('');
|
||||
try {
|
||||
const report = model();
|
||||
const blob = await generateProjectDossierDocx(
|
||||
report,
|
||||
selected,
|
||||
selectedSections.has('annexes'),
|
||||
);
|
||||
download(
|
||||
blob,
|
||||
`relatorio-investmoz-${filenameStem(report.title)}.docx`,
|
||||
);
|
||||
} catch (cause) {
|
||||
setError(dossierExportFailure('word', cause));
|
||||
} finally {
|
||||
setBusy('');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async function exportPackage(): Promise<void> {
|
||||
setBusy('package');
|
||||
setError('');
|
||||
try {
|
||||
const report = model();
|
||||
const docx = await generateProjectDossierDocx(
|
||||
report,
|
||||
selected,
|
||||
selectedSections.has('annexes'),
|
||||
);
|
||||
const archive = await generateProjectDossierPackage(
|
||||
docx,
|
||||
selected,
|
||||
selectedSections.has('annexes'),
|
||||
);
|
||||
download(
|
||||
archive,
|
||||
`relatorio-investmoz-${filenameStem(report.title)}-anexos.zip`,
|
||||
);
|
||||
} catch (cause) {
|
||||
setError(
|
||||
cause instanceof Error
|
||||
? cause.message
|
||||
: 'Não foi possível preparar o pacote de anexos.',
|
||||
);
|
||||
} finally {
|
||||
setBusy('');
|
||||
}
|
||||
await exportGate.current.run(async () => {
|
||||
setBusy('package');
|
||||
setError('');
|
||||
try {
|
||||
const report = model();
|
||||
const docx = await generateProjectDossierDocx(
|
||||
report,
|
||||
selected,
|
||||
selectedSections.has('annexes'),
|
||||
);
|
||||
const archive = await generateProjectDossierPackage(
|
||||
docx,
|
||||
selected,
|
||||
selectedSections.has('annexes'),
|
||||
);
|
||||
download(
|
||||
archive,
|
||||
`relatorio-investmoz-${filenameStem(report.title)}-anexos.zip`,
|
||||
);
|
||||
} catch (cause) {
|
||||
setError(dossierExportFailure('package', cause));
|
||||
} finally {
|
||||
setBusy('');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async function exportPdf(): Promise<void> {
|
||||
const printWindow = window.open('about:blank', '_blank');
|
||||
if (!printWindow) {
|
||||
setError(
|
||||
'O browser bloqueou a janela de impressão. Permita pop-ups para abrir a versão PDF.',
|
||||
);
|
||||
return;
|
||||
}
|
||||
setBusy('pdf');
|
||||
setError('');
|
||||
try {
|
||||
const html = await createProjectDossierPrintHtml(
|
||||
model(),
|
||||
selected,
|
||||
selectedSections.has('annexes'),
|
||||
);
|
||||
printWindow.document.open();
|
||||
printWindow.document.write(html);
|
||||
printWindow.document.close();
|
||||
} catch (cause) {
|
||||
printWindow.close();
|
||||
setError(
|
||||
cause instanceof Error
|
||||
? cause.message
|
||||
: 'Não foi possível preparar a versão para PDF.',
|
||||
);
|
||||
} finally {
|
||||
setBusy('');
|
||||
}
|
||||
await exportGate.current.run(async () => {
|
||||
const printWindow = window.open('about:blank', '_blank');
|
||||
if (!printWindow) {
|
||||
setError(
|
||||
'O browser bloqueou a janela de impressão. Permita pop-ups para abrir a versão PDF.',
|
||||
);
|
||||
return;
|
||||
}
|
||||
setBusy('pdf');
|
||||
setError('');
|
||||
try {
|
||||
const html = await createProjectDossierPrintHtml(
|
||||
model(),
|
||||
selected,
|
||||
selectedSections.has('annexes'),
|
||||
);
|
||||
printWindow.document.open();
|
||||
printWindow.document.write(html);
|
||||
printWindow.document.close();
|
||||
} catch (cause) {
|
||||
printWindow.close();
|
||||
setError(dossierExportFailure('pdf', cause));
|
||||
} finally {
|
||||
setBusy('');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function exportCsv(): void {
|
||||
const blob = new Blob([exportProjectDossierCsv(draft)], {
|
||||
type: 'text/csv;charset=utf-8',
|
||||
async function exportCsv(): Promise<void> {
|
||||
await exportGate.current.run(async () => {
|
||||
setBusy('csv');
|
||||
setError('');
|
||||
try {
|
||||
const blob = new Blob([exportProjectDossierCsv(draft)], {
|
||||
type: 'text/csv;charset=utf-8',
|
||||
});
|
||||
download(
|
||||
blob,
|
||||
`dossier-projecto-investmoz-${filenameStem(String(draft.project_title ?? ''))}.csv`,
|
||||
);
|
||||
} catch (cause) {
|
||||
setError(dossierExportFailure('csv', cause));
|
||||
} finally {
|
||||
setBusy('');
|
||||
}
|
||||
});
|
||||
download(
|
||||
blob,
|
||||
`dossier-projecto-investmoz-${filenameStem(String(draft.project_title ?? ''))}.csv`,
|
||||
);
|
||||
}
|
||||
|
||||
const totalBytes = attachments.reduce(
|
||||
@@ -261,6 +284,7 @@ export function ProjectDossierExport({
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
disabled={Boolean(busy)}
|
||||
checked={selectedSections.has(section.id)}
|
||||
onChange={(event) =>
|
||||
toggleSection(
|
||||
@@ -298,9 +322,10 @@ export function ProjectDossierExport({
|
||||
id="project-report-files"
|
||||
type="file"
|
||||
multiple
|
||||
disabled={Boolean(busy)}
|
||||
accept="image/jpeg,image/png,text/csv,text/plain,.csv,.tsv,.txt,.pdf,.doc,.docx,.xlsx,.odt"
|
||||
onChange={(event) => {
|
||||
addFiles(event.currentTarget.files);
|
||||
void addFiles(event.currentTarget.files);
|
||||
event.currentTarget.value = '';
|
||||
}}
|
||||
/>
|
||||
@@ -322,6 +347,7 @@ export function ProjectDossierExport({
|
||||
<input
|
||||
aria-label={`Incluir ${item.file.name}`}
|
||||
type="checkbox"
|
||||
disabled={Boolean(busy)}
|
||||
checked={item.include}
|
||||
onChange={(event) =>
|
||||
updateAttachment(index, {
|
||||
@@ -340,6 +366,7 @@ export function ProjectDossierExport({
|
||||
<button
|
||||
type="button"
|
||||
className="rounded border px-2 py-1 text-sm"
|
||||
disabled={Boolean(busy)}
|
||||
onClick={() => removeAttachment(index)}
|
||||
>
|
||||
Remover
|
||||
@@ -352,6 +379,7 @@ export function ProjectDossierExport({
|
||||
<input
|
||||
className="rounded border px-3 py-2"
|
||||
maxLength={300}
|
||||
disabled={Boolean(busy)}
|
||||
value={item.caption}
|
||||
onChange={(event) =>
|
||||
updateAttachment(index, {
|
||||
@@ -372,6 +400,11 @@ export function ProjectDossierExport({
|
||||
em zero. O relatório identifica pressupostos, estimativas e
|
||||
referências sem as apresentar como resultados confirmados.
|
||||
</p>
|
||||
{notice && (
|
||||
<p role="status" className="text-sm">
|
||||
{notice}
|
||||
</p>
|
||||
)}
|
||||
{error && (
|
||||
<p role="alert" className="text-sm text-red-800">
|
||||
{error}
|
||||
@@ -414,7 +447,7 @@ export function ProjectDossierExport({
|
||||
type="button"
|
||||
className="rounded border px-4 py-2"
|
||||
disabled={Boolean(busy)}
|
||||
onClick={exportCsv}
|
||||
onClick={() => void exportCsv()}
|
||||
>
|
||||
Exportar CSV complementar
|
||||
</button>
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
import {
|
||||
validateDossierAttachments,
|
||||
type DossierAttachment,
|
||||
} from './project-dossier-report.ts';
|
||||
|
||||
export type DossierExportOperation =
|
||||
| 'word'
|
||||
| 'package'
|
||||
| 'pdf'
|
||||
| 'csv'
|
||||
| 'attachments';
|
||||
const messages: Record<DossierExportOperation, string> = {
|
||||
word: 'Não foi possível gerar o relatório Word.',
|
||||
package: 'Não foi possível preparar o pacote de anexos.',
|
||||
pdf: 'Não foi possível preparar a versão para PDF.',
|
||||
csv: 'Não foi possível preparar o ficheiro CSV.',
|
||||
attachments: 'Não foi possível ler os anexos seleccionados.',
|
||||
};
|
||||
let failureSequence = 0;
|
||||
|
||||
export function dossierExportFailure(
|
||||
operation: DossierExportOperation,
|
||||
cause: unknown,
|
||||
log: (message: string, detail: unknown) => void = console.error,
|
||||
): string {
|
||||
const reference = `EXP-${Date.now().toString(36)}-${++failureSequence}`;
|
||||
// Local diagnostics only: never send dossier values or file contents.
|
||||
log('[InvesteMoz exportação]', { reference, operation, cause });
|
||||
return `${messages[operation]} Os dados, as secções e os anexos foram mantidos. Tente novamente pelo mesmo botão. Referência: ${reference}.`;
|
||||
}
|
||||
|
||||
/** Synchronous acquisition prevents a second click before React renders busy. */
|
||||
export class DossierExportGate {
|
||||
private active = false;
|
||||
async run(task: () => Promise<void>): Promise<boolean> {
|
||||
if (this.active) return false;
|
||||
this.active = true;
|
||||
try {
|
||||
await task();
|
||||
return true;
|
||||
} finally {
|
||||
this.active = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function prepareDossierAttachments(
|
||||
current: DossierAttachment[],
|
||||
incoming: File[],
|
||||
): Promise<{
|
||||
attachments: DossierAttachment[];
|
||||
duplicateCount: number;
|
||||
problem?: string;
|
||||
}> {
|
||||
for (const file of incoming) {
|
||||
const problem = validateDossierAttachments([file]);
|
||||
if (problem)
|
||||
return { attachments: current, duplicateCount: 0, problem };
|
||||
}
|
||||
const identity = async (file: File) => {
|
||||
const hash = await crypto.subtle.digest(
|
||||
'SHA-256',
|
||||
await file.arrayBuffer(),
|
||||
);
|
||||
return `${file.name}:${Array.from(new Uint8Array(hash), (b) => b.toString(16).padStart(2, '0')).join('')}`;
|
||||
};
|
||||
const known = new Set(
|
||||
await Promise.all(current.map((item) => identity(item.file))),
|
||||
);
|
||||
const added: DossierAttachment[] = [];
|
||||
let duplicateCount = 0;
|
||||
for (const file of incoming) {
|
||||
const key = await identity(file);
|
||||
if (known.has(key)) {
|
||||
duplicateCount++;
|
||||
continue;
|
||||
}
|
||||
known.add(key);
|
||||
added.push({ file, include: true, caption: '' });
|
||||
}
|
||||
const problem = validateDossierAttachments(
|
||||
[...current, ...added].map((item) => item.file),
|
||||
);
|
||||
return {
|
||||
attachments: problem ? current : [...current, ...added],
|
||||
duplicateCount,
|
||||
problem: problem ?? undefined,
|
||||
};
|
||||
}
|
||||
@@ -659,8 +659,13 @@ export async function createProjectDossierPrintHtml(
|
||||
content.push('<p>Não há informação fornecida nesta secção.</p>');
|
||||
content.push('</section>');
|
||||
}
|
||||
// Page margin boxes reserve footer space on every printed page. A fixed
|
||||
// body element can overlap fragmented tables in Chromium's print layout.
|
||||
const printFooter = JSON.stringify(
|
||||
`InvesteMoz · Relatório privado · ${model.version}`,
|
||||
).replaceAll('<', '\\3c ');
|
||||
return `<!doctype html><html lang="pt-MZ"><head><meta charset="utf-8"><title>Relatório — ${escapeHtml(model.title)}</title><style>
|
||||
@page{size:A4;margin:18mm 17mm 20mm}*{box-sizing:border-box}body{font:10.5pt/1.45 Arial,sans-serif;color:#17211f;margin:0}h1{font-size:24pt;color:#123b35;margin:0 0 7mm}h2{font-size:16pt;color:#123b35;border-bottom:1px solid #b8c9c3;padding-bottom:2mm;margin:8mm 0 3mm;break-after:avoid}h3{font-size:12pt;color:#27675c;break-after:avoid}p{white-space:pre-wrap}table{width:100%;border-collapse:collapse;table-layout:fixed;margin:3mm 0 5mm;break-inside:auto}th,td{border:1px solid #cbd5d1;padding:5px 6px;text-align:left;vertical-align:top;overflow-wrap:anywhere}th{background:#123b35;color:white}tr{break-inside:avoid}.report-table th:first-child,.report-table td:first-child{width:22%}.report-table th:last-child,.report-table td:last-child{width:32%}.report-table td:last-child{font-size:9pt;color:#40524d}.annex-table{table-layout:fixed;font-size:9pt}thead{display:table-header-group}.private{padding:3mm;background:#fff4df;border-left:3px solid #b66a0c;font-weight:bold}section{break-inside:auto}figure{margin:5mm auto;text-align:center;break-inside:avoid}img{max-width:100%;max-height:210mm;object-fit:contain}figcaption{font-style:italic;color:#52615d}pre{white-space:pre-wrap;overflow-wrap:anywhere;font:9.5pt/1.4 Arial,sans-serif}@media screen{body{max-width:1040px;margin:24px auto;padding:24px}table{font-size:11pt}}@media screen{body{max-width:1040px;margin:24px auto;padding:24px}table{font-size:11pt}}@media print{a{color:inherit;text-decoration:none}.no-print{display:none}th{color:#17211f;background:#edf4f1}footer{position:fixed;bottom:-14mm;left:0;right:0;font-size:9pt;color:#40524d}}
|
||||
@page{size:A4;margin:18mm 17mm 20mm;@bottom-left{content:${printFooter};font:9pt/1.3 Arial,sans-serif;color:#40524d;vertical-align:middle;width:80%}@bottom-right{content:counter(page) " / " counter(pages);font:9pt/1.3 Arial,sans-serif;color:#40524d;vertical-align:middle}}*{box-sizing:border-box}body{font:10.5pt/1.45 Arial,sans-serif;color:#17211f;margin:0}h1{font-size:24pt;color:#123b35;margin:0 0 7mm}h2{font-size:16pt;color:#123b35;border-bottom:1px solid #b8c9c3;padding-bottom:2mm;margin:8mm 0 3mm;break-after:avoid}h3{font-size:12pt;color:#27675c;break-after:avoid}p{white-space:pre-wrap}table{width:100%;border-collapse:collapse;table-layout:fixed;margin:3mm 0 5mm;break-inside:auto}th,td{border:1px solid #cbd5d1;padding:5px 6px;text-align:left;vertical-align:top;overflow-wrap:anywhere}th{background:#123b35;color:white}tr{break-inside:avoid}.report-table th:first-child,.report-table td:first-child{width:22%}.report-table th:last-child,.report-table td:last-child{width:32%}.report-table td:last-child{font-size:9pt;color:#40524d}.annex-table{table-layout:fixed;font-size:9pt}thead{display:table-header-group}.private{padding:3mm;background:#fff4df;border-left:3px solid #b66a0c;font-weight:bold}section{break-inside:auto}figure{margin:5mm auto;text-align:center;break-inside:avoid}img{max-width:100%;max-height:210mm;object-fit:contain}figcaption{font-style:italic;color:#52615d}pre{white-space:pre-wrap;overflow-wrap:anywhere;font:9.5pt/1.4 Arial,sans-serif}@media screen{body{max-width:1040px;margin:24px auto;padding:24px}table{font-size:11pt}}@media screen{body{max-width:1040px;margin:24px auto;padding:24px}table{font-size:11pt}}@media print{a{color:inherit;text-decoration:none}.no-print{display:none}th{color:#17211f;background:#edf4f1}footer{display:none}}
|
||||
</style></head><body><h1>InvesteMoz<br>Relatório consolidado do projecto</h1><p><strong>${escapeHtml(model.title)}</strong><br>Gerado em ${escapeHtml(new Date(model.generatedAt).toLocaleString('pt-MZ'))}<br>Versão/revisão: ${escapeHtml(model.version)}</p><p class="private">Documento privado. O portal não publica nem partilha este conteúdo.</p>${content.join('')}<footer><small>InvesteMoz · Relatório privado · ${escapeHtml(model.version)}</small></footer><script>window.addEventListener('load',()=>setTimeout(()=>window.print(),350));</script></body></html>`;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,141 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import {
|
||||
DossierExportGate,
|
||||
dossierExportFailure,
|
||||
prepareDossierAttachments,
|
||||
} from '../../resources/js/lib/dossier-export-state.ts';
|
||||
import {
|
||||
createProjectDossierReport,
|
||||
generateProjectDossierDocx,
|
||||
createProjectDossierPrintHtml,
|
||||
generateProjectDossierPackage,
|
||||
} from '../../resources/js/lib/project-dossier-report.ts';
|
||||
|
||||
test('concurrent export is ignored; failed generation unlocks one retry without mutating inputs', async () => {
|
||||
const gate = new DossierExportGate();
|
||||
const draft = {
|
||||
project_title: 'SINTÉTICO QA',
|
||||
project_summary: 'Pressupostos declarados; não é resultado fiscal.',
|
||||
};
|
||||
const saved = JSON.stringify(draft);
|
||||
let release;
|
||||
let starts = 0;
|
||||
const first = gate.run(async () => {
|
||||
starts++;
|
||||
await new Promise((resolve) => {
|
||||
release = resolve;
|
||||
});
|
||||
throw new Error('Library failure');
|
||||
});
|
||||
assert.equal(
|
||||
await gate.run(async () => {
|
||||
starts++;
|
||||
}),
|
||||
false,
|
||||
);
|
||||
release();
|
||||
await assert.rejects(first, /Library failure/);
|
||||
assert.equal(
|
||||
await gate.run(async () => {
|
||||
starts++;
|
||||
}),
|
||||
true,
|
||||
);
|
||||
assert.equal(starts, 2);
|
||||
assert.equal(JSON.stringify(draft), saved);
|
||||
});
|
||||
|
||||
test('all operations show Portuguese recovery text while retaining technical details in diagnostics', () => {
|
||||
for (const operation of ['word', 'package', 'pdf', 'csv', 'attachments']) {
|
||||
const error = new Error('English internals / private stack');
|
||||
const logs = [];
|
||||
const text = dossierExportFailure(operation, error, (...args) =>
|
||||
logs.push(args),
|
||||
);
|
||||
assert.match(text, /Não foi possível/);
|
||||
assert.match(text, /foram mantidos.*Tente novamente/);
|
||||
assert.doesNotMatch(text, /English internals|private stack/);
|
||||
assert.equal(logs.length, 1);
|
||||
assert.equal(logs[0][1].cause, error);
|
||||
assert.ok(text.includes(logs[0][1].reference));
|
||||
}
|
||||
});
|
||||
|
||||
test('attachment read failure and size refusal preserve existing selection; retry deduplicates bytes and keeps captions', async () => {
|
||||
const file = new File(['SINTÉTICO QA'], 'qa.txt', { type: 'text/plain' });
|
||||
const current = [{ file, include: false, caption: 'Legenda conservada' }];
|
||||
const broken = new File(['another'], 'broken.txt');
|
||||
broken.arrayBuffer = async () => {
|
||||
throw new Error('OS read failed');
|
||||
};
|
||||
await assert.rejects(
|
||||
prepareDossierAttachments(current, [broken]),
|
||||
/OS read failed/,
|
||||
);
|
||||
assert.equal(current.length, 1);
|
||||
const tooLarge = new File([new Uint8Array(16 * 1024 * 1024)], 'large.txt');
|
||||
const refused = await prepareDossierAttachments(current, [tooLarge]);
|
||||
assert.equal(refused.attachments, current);
|
||||
assert.match(refused.problem, /15 MB/);
|
||||
const result = await prepareDossierAttachments(current, [
|
||||
new File(['SINTÉTICO QA'], 'qa.txt'),
|
||||
new File(['distinct'], 'qa.txt'),
|
||||
]);
|
||||
assert.equal(result.duplicateCount, 1);
|
||||
assert.equal(result.attachments.length, 2);
|
||||
assert.equal(result.attachments[0], current[0]);
|
||||
assert.equal(result.attachments[0].caption, 'Legenda conservada');
|
||||
assert.equal(result.attachments[0].include, false);
|
||||
});
|
||||
|
||||
test('actual Word PDF and ZIP generators recover after a transient attachment read failure; archive has one original', async () => {
|
||||
const model = createProjectDossierReport(
|
||||
{ project_title: 'QA recuperação' },
|
||||
{ selectedSections: new Set(['summary', 'annexes']) },
|
||||
);
|
||||
const file = new File(['Texto QA'], 'qa.txt', { type: 'text/plain' });
|
||||
const original = file.text.bind(file);
|
||||
let fail = false;
|
||||
file.text = async () => {
|
||||
if (fail) {
|
||||
fail = false;
|
||||
throw new Error('Transient attachment read failure');
|
||||
}
|
||||
return original();
|
||||
};
|
||||
const attachments = [{ file, include: true, caption: '' }];
|
||||
for (const generate of [
|
||||
() => generateProjectDossierDocx(model, attachments, true),
|
||||
() => createProjectDossierPrintHtml(model, attachments, true),
|
||||
]) {
|
||||
fail = true;
|
||||
await assert.rejects(generate, /Transient attachment/);
|
||||
assert.ok(await generate());
|
||||
assert.equal(attachments.length, 1);
|
||||
}
|
||||
const word = await generateProjectDossierDocx(model, [], false);
|
||||
let broken = true;
|
||||
const { DossierExportGate: Gate } =
|
||||
await import('../../resources/js/lib/dossier-export-state.ts');
|
||||
const gate = new Gate();
|
||||
await assert.rejects(
|
||||
gate.run(async () => {
|
||||
if (broken) throw new Error('ZIP write fault');
|
||||
}),
|
||||
/ZIP write fault/,
|
||||
);
|
||||
broken = false;
|
||||
let zip;
|
||||
await gate.run(async () => {
|
||||
zip = await generateProjectDossierPackage(word, attachments, true);
|
||||
});
|
||||
const { default: JSZip } = await import('jszip');
|
||||
const archive = await JSZip.loadAsync(await zip.arrayBuffer());
|
||||
assert.deepEqual(
|
||||
Object.keys(archive.files).filter(
|
||||
(name) => name.startsWith('Anexos/') && !archive.files[name].dir,
|
||||
),
|
||||
['Anexos/qa.txt'],
|
||||
);
|
||||
});
|
||||
@@ -295,6 +295,15 @@ void test('print/PDF HTML escapes user content and explains omitted annexes', as
|
||||
assert.match(pdf, /<script>/);
|
||||
assert.match(pdf, /não foram seleccionados|secção de anexos foi omitida/);
|
||||
assert.match(pdf, /@page\{size:A4/);
|
||||
assert.match(pdf, /@bottom-left\{content:/);
|
||||
assert.match(pdf, /@bottom-right\{content:counter\(page\)/);
|
||||
assert.doesNotMatch(pdf, /position:fixed|bottom:-14mm/);
|
||||
const escapedVersion = await createProjectDossierPrintHtml(
|
||||
{ ...report, version: 'Revisão "QA" </style><script>alert(2)</script>' },
|
||||
[],
|
||||
false,
|
||||
);
|
||||
assert.doesNotMatch(escapedVersion, /<script>alert\(2\)<\/script>/);
|
||||
});
|
||||
|
||||
void test('attachment limits block oversized or excessive selections', () => {
|
||||
|
||||
Reference in New Issue
Block a user