Compare commits

...
13 changed files with 729 additions and 41 deletions
@@ -2001,5 +2001,379 @@
"qa_fixtures_copied": false,
"accepted_scenarios_repeated_manually": false,
"limitations": "No new scenario executed in homologation and no real dossier or personal data created. QA functional evidence reused; destination verification limited to revision, configuration and entry routes. Sector/fiscal exclusions remain explicit."
},
"closure_scope_reconciliation_20261024": {
"kind": "analysis_and_proposed_batch_only",
"reference_commit": "276d833a4b4f9071db328b653f48529b2dfac4f9",
"scope": [
"Financial planning consistency",
"Existing published IVA on net monthly sales",
"Data/calculation/save/recovery/report continuity",
"Screen/export values/context/source/version agreement",
"Explicit unavailable functionality without presumed results"
],
"accepted_proofs_reused": [
"Independent financial controls and cashflow recovery",
"Published IVA evaluator, server selection and frozen versions",
"Monthly sales reuse, zero versus missing, stale history",
"General CSV/Word/ZIP and native footer",
"Selected opportunity native PDF, 8036b9b0/cdf8fcfb"
],
"code_presence_reconciled": [
"3f5430c3 is ancestor of reference/stabilization-20261024",
"0b0cf761 is ancestor of reference/stabilization-20261024"
],
"public_availability_read": {
"url": "https://teste-investidor.tempo.co.mz/api/v1/fiscal-consumer",
"enabled": true,
"coverage_start": "2026-01-01",
"coverage_end": "2026-12-31",
"not_a_fresh_private_rule_audit": true
},
"items": [
{
"id": "S1",
"classification": "code_gap_new_isolated_diagnostic",
"consumer": "Saved cashflow to IVA fallback and report",
"cause": "Cashflow snapshot v1 has no original currency; consumers use current project currency",
"evidence": "outputs/simulator-scope-20261024/currency-gap.json",
"observed": "Saved amounts with original project USD become monthly IVA 123 MZN and MZN report after project currency changes; original currency absent from snapshot",
"proposal": "Version currency provenance; common mismatch/reconciliation guard; retain v1 bytes and require only missing currency where origin cannot be recovered",
"delta_proof": "Currency change, saved recovery and matching panel/IVA/report without conversion or loss"
},
{
"id": "S2",
"classification": "code_gap_directed_UI_proof_pending",
"consumer": "Monthly IVA pending execution recovery",
"cause": "Availability guard returns before pending-ID GET; button disabled when new admissions disabled",
"existing_server_contract": "Admitted execution/version and authenticated history remain readable",
"proposal": "Separate creation from read/resume by ID; do not reactivate consumer or create another execution",
"delta_proof": "Already admitted queued/running execution, new admissions unavailable, same ID recovered completed and reported without duplicate"
},
{
"id": "S3",
"classification": "specific_evidence_gap_not_presumed_defect",
"consumer": "Monthly IVA section in final dossier CSV/Word/native PDF",
"proposal": "One representative existing synthetic snapshot; compare exact values, zero/missing, selected context, source/version and current/history label; inspect only new monthly block pagination",
"reuse": "General exporters, ZIP, native footer and other accepted pagination proofs"
}
],
"proposed_order": [
"Fix S1 and S2 together",
"Check only changed dependencies",
"Complete S3 on resulting candidate, fixing only observed failures",
"Integrate and install through existing Git process after directed QA; verify installed delta"
],
"documentary_dependencies_identified": [],
"six_legacy_associations_and_two_incomplete_originals": "Remain documentary; no dependency identified for S1-S3",
"excluded": [
"Other taxes",
"New IVA rates/regimes/eligibility or incentives",
"Deductions/credits/refunds/final settlement",
"Full Modelo A",
"Extension beyond demonstrated 2026 coverage",
"Statistics and AI"
],
"functional_fixes_executed": false,
"deployment_executed": false,
"active_business_data_writes": 0,
"accepted_tests_reexecuted": false,
"historical_output_files_missing_in_this_workspace_do_not_revoke_acceptance": true,
"matrix_ids": [
"AUD-SIM-01",
"AUD-SIM-INT02",
"AUD-SIM-INT03",
"AUD-SIM-INT04",
"AUD-SIM-INT05",
"AUD-SIM-INT06",
"AUD-SIM-INT07",
"AUD-SIM-INT08"
]
},
"closure_qa_20261024": {
"status": "S1_S2_S3_passed_QA_ready_for_integration",
"authorization": "Tempo execute S1/S2 jointly in QA, then S3; no fiscal rule changes; candidate for existing Git integration",
"functional_candidate": "65b87819a0629f79f415788a6aa1a16fe031e37e",
"branch": "codex/simulator-closure-qa-20261024",
"preserved_reference": "276d833a4b4f9071db328b653f48529b2dfac4f9",
"integrated_main": false,
"homologation_deployed": false,
"S1": {
"status": "corrected_and_demonstrated_QA",
"snapshot_version": 2,
"currency_origin_preserved": true,
"original_USD_to_project_MZN": "guarded; report/panel retain USD, IVA refuses reuse as MZN",
"explicit_resolution": "Restore original currency on project; unknown legacy origin requires explicit currency confirmation only",
"legacy_v1_inputs_preserved": true,
"private_draft_roundtrip_passed": true
},
"S2": {
"status": "corrected_and_demonstrated_QA",
"method": "GET same admitted execution ID, credentials same-origin; wrong response ID rejected",
"new_admissions_disabled": true,
"visual_read_requests": 1,
"visual_create_requests": 0,
"real_API_owner_read_and_wrong_owner_denial": true,
"execution_count_preserved": true,
"rule_version_fingerprint_unchanged": true
},
"S3": {
"status": "final_CSV_Word_and_Edge_PDF_verified",
"artifact_inspection": {
"files": {
"monthly-final.csv": {
"bytes": 10639,
"sha256": "0b5fc670b0c252391aa99c0bff4e21af1e7a7b9c3a0e4ac6e2e2359a647d8a0e"
},
"monthly-final.docx": {
"bytes": 14101,
"sha256": "f83629e6ed3090a0d99d4843b11d4388048a6c993d2e1f7fabd87282bf0c2b38"
},
"monthly-final.html": {
"bytes": 11530,
"sha256": "09d40095550cae9bfdd29fcb5c6c7e0ad825a26170997a23347a8320a352b3a7"
}
},
"same_frozen_monthly_fields_verified": [
"160.0048",
"320.0096",
"1160.0348",
"2320.0696",
"MZN",
"opportunity:309",
"Manica",
"2026-01-01",
"2026-04-01",
"Fonte sintética QA",
"IVA-GERAL-BASE-LIQUIDA"
],
"csv_rows": 73,
"first_opportunity_not_included": true,
"fiscal_calculation_repeated": false,
"native_pdf_status": "awaiting_saved_file; request already sent"
},
"native_pdf_expected_path": "C:/laragon/www/portal-investidor/outputs/simulator-closure-qa-20261024/monthly-final.pdf",
"user_response": "Will save at requested path",
"HTML_is_not_native_pdf_proof": true,
"fixture": "Explicit synthetic QA, frozen accepted monthly numbers; not legal validation or real-rule publication",
"pdf_generation": {
"renderer": "Installed Microsoft Edge, isolated headless print engine",
"manual_print_dialog_used": false,
"source": "C:\\laragon\\www\\portal-investidor\\outputs\\simulator-closure-qa-20261024\\ui\\monthly-review.html",
"source_sha256": "b44ee5fc573f8deb8d5427f9f3b6a667c1d60fc23de4509d383b23acb9df2779",
"pdf": "C:\\laragon\\www\\portal-investidor\\outputs\\simulator-closure-qa-20261024\\monthly-final.pdf",
"bytes": 111910,
"sha256": "7719a05136f6dba668b621f551626060d10db5fcbb84815cdad112a1eab9079b",
"exit_code": 0
},
"pdf_inspection": {
"status": "native_file_read_and_rendered",
"exact_path": "C:\\laragon\\www\\portal-investidor\\outputs\\simulator-closure-qa-20261024\\monthly-final.pdf",
"sha256": "7719a05136f6dba668b621f551626060d10db5fcbb84815cdad112a1eab9079b",
"pages": [
{
"page": 1,
"size": [
594.9599609375,
841.9199829101562
],
"characters": 1633,
"image": "C:\\laragon\\www\\portal-investidor\\outputs\\simulator-closure-qa-20261024\\monthly-pdf-pages\\page-01.png",
"footer_boxes": [
[
48.0,
807.8525390625,
257.5710144042969,
817.9072265625
]
],
"footer_separated": true
},
{
"page": 2,
"size": [
594.9599609375,
841.9199829101562
],
"characters": 2299,
"image": "C:\\laragon\\www\\portal-investidor\\outputs\\simulator-closure-qa-20261024\\monthly-pdf-pages\\page-02.png",
"footer_boxes": [
[
48.0,
807.8525390625,
257.5710144042969,
817.9072265625
]
],
"footer_separated": true
},
{
"page": 3,
"size": [
594.9599609375,
841.9199829101562
],
"characters": 2047,
"image": "C:\\laragon\\www\\portal-investidor\\outputs\\simulator-closure-qa-20261024\\monthly-pdf-pages\\page-03.png",
"footer_boxes": [
[
48.0,
807.8525390625,
257.5710144042969,
817.9072265625
]
],
"footer_separated": true
},
{
"page": 4,
"size": [
594.9599609375,
841.9199829101562
],
"characters": 1832,
"image": "C:\\laragon\\www\\portal-investidor\\outputs\\simulator-closure-qa-20261024\\monthly-pdf-pages\\page-04.png",
"footer_boxes": [
[
48.0,
807.8525390625,
257.5710144042969,
817.9072265625
]
],
"footer_separated": true
}
],
"checks": {
"160.0048": true,
"320.0096": true,
"1160.0348": true,
"2320.0696": true,
"MZN": true,
"opportunity:309": true,
"Manica": true,
"2026-01-01": true,
"2026-04-01": true,
"Fonte sintética QA": true,
"IVA-GERAL-BASE-LIQUIDA": true,
"first_opportunity_absent": true,
"rule_version_4": true,
"zero_and_missing": true
},
"content_checks_passed": true,
"visual_review": "passed: all four rendered pages inspected; repeated headers, intact rows, no clipping/overlap, footers separated, numbering 1/4-4/4",
"regenerated": false,
"render_method": "Installed Microsoft Edge headless print engine, isolated profile; not manual print-dialog save",
"generation_authorized_by_user": true
},
"manual_print_dialog_used": false,
"regeneration_authorization": "User explicitly requested generating the file again to complete this step"
},
"checks": {
"frontend_new_cases": 4,
"frontend_total_direct_dependencies": 12,
"frontend_passed": 12,
"API_tests": 2,
"API_assertions": 23,
"QA_database": "simulator_fiscal_delta_20261021_test",
"QA_port": 55441,
"QA_transactions_rolled_back": true,
"typescript": "passed",
"build": "passed",
"diff_check": "passed"
},
"unchanged": [
"Fiscal server/evaluator/rules/sources",
"Schema/configuration/permissions/routes",
"Active business data",
"Accepted stabilization and opportunity-context closures"
],
"remaining": [],
"ready_for_integration": true,
"new_taxes_added": false,
"integration_note": "Candidate only; existing Git process and installed-delta verification still to execute after integration authorization; no homologation delivery claimed"
},
"closure_homologation_20261024": {
"status": "installed_delta_verified",
"authorization": "Tempo current conversation: integrate S1-S3 through existing Git process, install homologation and verify only delta",
"served_commit": "cd10cccbf45f53836d8bf7b2bd90794d99602cb9",
"functional_candidate": "65b87819",
"QA_closure_record": "cd10cccb",
"previous_main": "276d833a4b4f9071db328b653f48529b2dfac4f9",
"stabilization_tag": "reference/stabilization-20261024",
"Git_integration": "fast-forward after fetch; no conflicting/concurrent main changes found; no force",
"installed_delta": {
"served_revision": {
"commit": "cd10cccbf45f53836d8bf7b2bd90794d99602cb9",
"branch": "main",
"built_at": "2026-10-24T09:39:42.304316+00:00",
"manifest_sha256": "7957c776ab2c2eab516bcc6caedcdeb9943184645b4a21a89a4f709a4ba974f6"
},
"manifest_hash_matches": true,
"compiled_delta_markers": {
"currency_origin_guard": "assets/project-dossier-DNSA8_QR.js",
"original_currency_report": "assets/project-dossier-DNSA8_QR.js",
"same_id_recovery": "assets/services-BXcNtigY.js",
"explicit_currency_resolution": "assets/services-BXcNtigY.js"
},
"assets": [
{
"file": "assets/project-dossier-DNSA8_QR.js",
"status": 200,
"content_type": "application/javascript",
"sha256": "8cfe6743dddc5f35ddfe7ccdd6bb4d859719c1e263c6e51d9ac94b30a5fe1b76"
},
{
"file": "assets/simulation-form-ClLDsvMf.js",
"status": 200,
"content_type": "application/javascript",
"sha256": "fc829b447dcb1eec1cc550edcbfaa28f4db7b8973b4f86dab8a065cbbd8f0421"
},
{
"file": "assets/project-dossier-document-CJ_IVxDn.js",
"status": 200,
"content_type": "application/javascript",
"sha256": "ac4013924c4a78427c3e134654f331bf6fce313733532e7084ed85cf344106b2"
},
{
"file": "assets/services-BXcNtigY.js",
"status": 200,
"content_type": "application/javascript",
"sha256": "66d8af6670347a19e4006323f6c8b84e625a2dbaad6b4de777b4d979faa67093"
}
],
"simulator_public_status": 200,
"simulator_uses_installed_app_entry": "assets/app-Ds1F0RnS.js",
"existing_fiscal_availability": {
"data": {
"enabled": true,
"coverage_start": "2026-01-01",
"coverage_end": "2026-12-31",
"message": "IVA geral sobre base líquida disponível; enquadramento declarado."
}
},
"synthetic_dossiers_requests_or_calculations_created": 0,
"QA_acceptance_tests_repeated": false,
"method": "Read-only public installed delta: revision, manifest, compiled changed features, simulator page and existing availability; no authenticated DB audit claimed",
"shared_guard_asset_matches_QA_bytes": true,
"reference_276d833a_is_ancestor": true,
"fiscal_server_schema_config_routes_deploy_diff": [],
"cross_environment_asset_validation": "Served build checked against its own revision manifest and compiled changed features; not a claim of identical complete bundles across Windows QA and Linux production"
},
"functional_files": [
"resources/js/components/simulation-cashflow-planner.tsx",
"resources/js/components/simulation-monthly-iva.tsx",
"resources/js/lib/monthly-cashflow.ts",
"resources/js/lib/monthly-iva.ts",
"resources/js/lib/project-dossier.ts"
],
"preservation": "Stabilization ancestry and accepted proofs preserved; no fiscal server/schema/config/routes/deploy diff. No new authenticated database count audit claimed. No destination business-data mutation or new synthetic calculation requested.",
"accepted_QA_tests_repeated": false,
"PDF_regenerated_for_delivery": false,
"remaining_in_S1_S2_S3": [],
"legacy_snapshot_limit": "Original currency absent in v1 requires explicit confirmation; amounts preserved, MZN not presumed",
"not_full_all_tax_completion": true,
"next_fiscal_batch": "Scope from attached models; no automatic new tax/rule activation",
"statistics_and_AI": "remain deferred"
}
}
@@ -0,0 +1,80 @@
{
"reference_commit": "276d833a4b4f9071db328b653f48529b2dfac4f9",
"runtime_files_changed_since_066707ec": [],
"matrix_records": 3423,
"matrix_columns": 49,
"updated_existing_rows": [
"AUD-GEO-01",
"AUD-GEO-02",
"AUD-GEO-03",
"AUD-REL-01",
"AUD-REL-02",
"AUD-DESK-01",
"AUD-DESK-02",
"AUD-SIM-01",
"AUD-LEG-06"
],
"accepted_receipts_unchanged_sha256": {
"map-stabilization-receipt-20261024.json": "2b69216b644f2244897431a10f3e6828e822d1317fab326a457a5934261c1618",
"anchor-integration-receipt-20261023.json": "86b0d01aa2fb03be84c1b1c6254ea0932abe813375f00560aaf39ce78d199f3d",
"treatment-scope-installation-receipt-20261023.json": "a0904250e1ed201276fce0318b9cc131edf4c8ec0ab6d449536b1e1344906798",
"multi-opportunity-context-installation-receipt-20261023.json": "3e1030de9aafd916252926a33d18cf808a05e389ddce5d9a3cd5230f039bc8f2"
},
"new_tests": {
"tests": 2,
"assertions": 8,
"passed": true
},
"active_database_writes": 0,
"status": "installed_reference_verified",
"date_Africa_Johannesburg": "2026-10-24",
"git_tag": "reference/stabilization-20261024",
"served_revision": {
"commit": "276d833a4b4f9071db328b653f48529b2dfac4f9",
"branch": "main",
"built_at": "2026-10-24T09:10:08.359035+00:00",
"manifest_sha256": "949d1047867f5879e724e5ddc38d4865b06aeb92641337d3952ea7b5ad55bb7a"
},
"previous_functional_revision": "066707ece631f0a7c963031c211844fbdd68916b",
"verification_method": "New unit control tested; semantic matrix/inventory reconciliation; canonical accepted receipt hashes; empty Git diff for runtime/schema/config/permissions/deploy; public installed revision. Prior accepted technical and visual proofs reused.",
"accepted_tests_repeated_by_this_review": false,
"automatic_installer": "Normal existing pipeline; no extra manual acceptance round",
"fresh_authenticated_database_read_claimed": false,
"business_data_preservation": "Accepted map receipt 5d45541a reused: 29 fiches, 28 links, 408 geometries, 172 business/nonvolatile tables equal; no runtime or data mutation introduced by this reference delta",
"known_open_stabilization_faults": [],
"documentary_pending": [
"AUD-REL-02: six legacy opportunity/anchor-cluster pairs remain indeterminate without documented relationships",
"Two incomplete originals in the legacy collection retain the accepted documentary limit"
],
"not_yet_proven_existing_components": [
6,
18,
21,
22,
24,
25,
35
],
"future_requirements": [
"AUD-MARKET-01",
"AUD-ESTABLISHMENT-01",
"AUD-FUND-01",
"AUD-COMPANY-01",
"AUD-COMPANY-02",
"AUD-CAPITAL-01",
"AUD-CAPITAL-02",
"AUD-FINTECH-01"
],
"technical_preservation_owner": "Codex and programmers",
"Tempo_evaluation": "Usefulness, clarity, presentation; does not need to rediscover technical regressions",
"next": "Fiscal simulator scope definition; not started by this consolidation",
"reference_sequence": [
"Stabilization",
"Defined fiscal simulator scope",
"Real estate",
"Consumer goods and franchises",
"Statistics and dashboards",
"AI"
],
"not_complete_portal_acceptance": true
}
@@ -1,5 +1,59 @@
# Rastreabilidade, utilização e formação
## Simulador S1–S3 — entregue na homologação em `cd10cccb`, 24/10/2026
Por autorização expressa da Tempo após o fecho QA, integrado pelo Git existente, de `276d833a` para **`cd10cccbf45f53836d8bf7b2bd90794d99602cb9`**, que contém a correcção funcional `65b87819`. `git fetch` não encontrou alterações concorrentes em main; avanço normal, sem force. A referência `276d833a`/`reference/stabilization-20261024` permanece ancestral. Não houve edição da pasta servida nem migração/importação solicitada.
**Delta instalado conferido por leitura pública:** `/build/revision.json` identifica `cd10cccb`; SHA256 do manifesto servido coincide com o da revisão (`7957c776ab2c2eab516bcc6caedcdeb9943184645b4a21a89a4f709a4ba974f6`). Quatro assets do simulador/relatório responderam 200; os bundles contêm a guarda da moeda original, a confirmação explícita e a recuperação por mesmo ID. O asset comum da guarda de moeda coincide por bytes com o build QA. O simulador responde 200 e referencia a entrada app do manifesto instalado. O consumidor IVA mantém disponibilidade `enabled=true` e cobertura de 2026. Não se exige identidade dos bundles completos entre Windows QA e Linux; a conferência usa o manifesto do próprio build servido, os assets e o delta de código.
O diff de `app`, `database`, `config`, `routes` e `deploy` contra `276d833a` é vazio: sem alteração de motor, regras/fontes fiscais, schema, permissões, configuração ou processo de entrega. Cinco fontes frontend constituem o delta funcional de moeda/recuperação/relatório. Não foram criados dossiers, pedidos ou cálculos sintéticos na homologação, nem repetidos os ensaios aceites. Provas QA 12 frontend, 2 API/23 asserções, TypeScript/build e PDF de quatro páginas são reutilizadas, sem nova impressão. Não se apresenta esta leitura pública como auditoria autenticada nova dos dados protegidos.
**Saldo S1–S3: entregue, sem pendências deste lote.** Nos snapshots antigos sem moeda, a confirmação explícita da origem continua necessária, preservando os montantes; não se presume MZN nem se convertem valores. Este fecho abrange moeda da tesouraria, recuperação do IVA mensal e exportações; **não conclui todos os impostos**. Os modelos anexados delimitam o próximo lote fiscal, sem activação implícita. As pendências documentais anteriores mantêm-se no seu âmbito. Recibo integrado existente: chave `closure_homologation_20261024`; prova do delta em `outputs/simulator-delivery-20261024/installed-delta.json`. Os estados QA/pendentes abaixo são histórico superado por esta entrega.
## Simulador S1/S2/S3 — candidata QA `65b87819`, 24/10/2026
**Actualização de fecho S3:** a Tempo pediu expressamente voltar a gerar o ficheiro. O PDF foi gravado directamente em `outputs/simulator-closure-qa-20261024/monthly-final.pdf` pelo motor de impressão do Microsoft Edge em modo headless, com perfil isolado, a partir da mesma saída HTML final; não foi gravação manual pelo diálogo de impressão. Ficheiro de 111 910 bytes, SHA256 `7719a05136f6dba668b621f551626060d10db5fcbb84815cdad112a1eab9079b`. As **quatro páginas A4** foram extraídas, renderizadas e inspeccionadas visualmente: valores exactos, MZN, oportunidade `309`/Manica, Janeiro–Abril/2026, fonte sintética QA e versão 4 conservados; zero distinto de falta, cabeçalhos repetidos, linhas intactas, rodapés separados e numeração 1/4–4/4. CSV e Word previamente conferidos mantidos, sem recálculo fiscal. **S1, S2 e S3 concluídos em QA; candidata pronta para integração pelo Git.** Saldo QA deste lote: zero; integração, disponibilização e conferência do delta instalado ainda não executadas. Os estados de espera abaixo são histórico anterior a esta geração/conferência e não reabrem S3. Recibo integrado e mesmas oito linhas da matriz actualizados; referência `276d833a` e regras fiscais preservadas.
Execução autorizada pela Tempo após `be62783b`, preservando `276d833a` e a etiqueta `reference/stabilization-20261024`. Ramo `codex/simulator-closure-qa-20261024`; **não integrado em main nem entregue na homologação**. Cinco fontes frontend alteradas; sem alterações em motor/regras/fontes fiscais, schema, configuração, permissões, rotas ou dados activos. Sem impostos adicionais.
| Recorte | Correcção e resultado dirigido |
| --- | --- |
| **S1 — corrigido e demonstrado em QA** | Novos snapshots de tesouraria v2 conservam moeda de origem. Painel, CSV próprio e relatório usam essa moeda; o fallback de vendas para IVA recusa divergência. USD → MZN conserva `123 USD`, explica a diferença e permite restaurar USD no projecto por acção explícita. Os montantes não são convertidos nem reinterpretados. Snapshots v1 continuam legíveis; moeda sem origem recuperável exige confirmação única, conservando valores/modo/meses vazios. Não se presume MZN. A observação visual detectou um rótulo residual de saldo na moeda actual; corrigido para a moeda de origem. O rótulo do selector passou a «Moeda do projecto», distinguindo-o dos pressupostos guardados. |
| **S2 — corrigido e demonstrado em QA** | Recuperação mensal usa GET no mesmo ID, com credenciais e verificação do identificador devolvido; não depende da disponibilidade para novas admissões. Botão específico continua disponível com consumidor desligado. Resposta não substitui snapshot alterado entretanto ou componente desmontado. Interface isolada: consumidor indisponível, uma consulta GET, zero criações POST, mesma execução concluída. API real de QA: execução admitida → consumidor desligado só na configuração do teste → mesmo ID em fila/concluído; outra conta continua recusada e contagem de execuções não aumenta. Regras existentes reutilizadas, sem criar/publicar/suspender regra; fingerprint integral das versões igual. |
| **S3 — CSV/Word e fonte de impressão conferidos; PDF nativo ainda pendente** | Mesmo bloco mensal sintético congelado, sem recalcular: Janeiro `160.0048`, Fevereiro `320.0096`, Março zero, Abril sem informação; MZN, oportunidade `309`/Manica, período Janeiro–Abril/2026, fonte identificada como sintética QA e versão 4. CSV e Word finais lidos e comparados, incluindo ausência da primeira oportunidade/Sofala. A fonte HTML conserva os mesmos dados. O relatório está aberto no Edge e a Tempo indicou que guardará o PDF em `outputs/simulator-closure-qa-20261024/monthly-final.pdf`; falta ler e renderizar esse ficheiro para conferir apenas a paginação do bloco. HTML não substitui essa prova. |
Provas: quatro testes novos em `simulator-closure-scope.test.mjs`, mais oito casos das dependências directamente afectadas, **12/12 aprovados**; `SimulatorClosureRecoveryTest`, **2 casos/23 asserções**, transacções revertidas no clone nominal `simulator_fiscal_delta_20261021_test:55441`. TypeScript e build aprovados. As suites de aceitação não afectadas, ZIP, cálculos fiscais, PDFs/rodapé anteriores e contextos por oportunidade são reutilizados; não se exigiu à Tempo repetir essas provas. O ensaio visual usa componentes reais com transporte simulado explicitamente identificado, complementado pelo teste API de permissões; não é apresentado como servidor real ou processo na homologação.
Artefactos novos: `outputs/simulator-closure-qa-20261024/` — CSV/Word/HTML finais, snapshot congelado, inspecção/hash dos ficheiros, captura de recuperação e resultados dos testes. O estado executado fica no recibo integrado existente, chave `closure_qa_20261024`, e nas mesmas oito linhas da matriz. **Limite corrente:** S1/S2 em QA; S3 aguarda somente o PDF nativo guardado. A candidata não é declarada pronta para integração enquanto faltar essa conferência. Os seis pares legados e os dois originais incompletos não têm dependência efectiva neste lote.
## Simulador — âmbito de fecho reconciliado e lote proposto, 24/10/2026
Pedido expresso da Tempo após a referência `276d833a`/`reference/stabilization-20261024`, confirmada segundo o relatório, sem lhe atribuir leitura do recibo local. Esta passagem é **análise e proposta**, sem correcção funcional, integração, nova publicação/activação fiscal ou alteração de dados. A ordem de referência mantém-se. Não se acrescentam impostos nem se reabre o lote de estabilização.
### O que já existe e permanece aceite
| Frente | Implementação e provas reutilizadas |
| --- | --- |
| Planeamento financeiro | Plano empresarial por produto/mês, tesouraria, pressupostos editáveis, moeda e cenários de planeamento; controlos independentes anteriores de saldos positivo/negativo/nulo/incompleto, crédito e projecções. `business-financial-plan`, `monthly-cashflow` e recibos integrados de 20–22/10. Exemplos demonstrativos e imposto manual continuam identificados como planeamento, sem serem taxas legais ou valores do investidor. |
| IVA implementado | Consumidor publicado, selecção e versão congeladas no servidor, avaliador existente; vendas mensais do plano ou receita constante já guardada, indicação explícita de base líquida, zero distinto de falta e IVA separado da caixa. `SimulationPublishedFiscalTest`, quatro casos de `monthly-iva.test.mjs` e ensaio Edge mensal anteriores reutilizados. `3f5430c3` e `0b0cf761` pertencem à referência instalada; as notas históricas «só QA» não significam hoje ausência deste código. A disponibilidade pública consultada nesta passagem devolveu `enabled=true`, cobertura 2026. Isso não é uma nova auditoria dos parâmetros protegidos; o cadastro/publicação real v1 mantém o recibo de `d3fe8a02`. |
| Continuidade | Dois dossiers/nova sessão, isolamento, recuperação, erro e nova tentativa, históricos e hashes já provados. A escolha da oportunidade e o PDF correspondente estão entregues em `8036b9b0`/`cdf8fcfb`; não repetir. Inputs, rascunho e snapshots usam os mecanismos existentes, sem voltar a preencher vendas no IVA. |
| Resultados/exportações | Resumo/detalhe, CSV/Word, erro e nova tentativa de exportação, ZIP e rodapé do PDF nativo têm provas aceites. O rodapé nativo de tesouraria foi corrigido e conferido em 21/10; o PDF guardado por oportunidade foi conferido em 23/10. O bloco mensal usa o mesmo snapshot fiscal e não recalcula imposto ao exportar. A prova específica restante desse bloco consta de S3 abaixo; não reabrir os restantes exportadores ou PDFs. |
| Indisponibilidade | Fora da cobertura demonstrada de 2026, moeda diferente de MZN, preços com IVA incluído/base desconhecida, enquadramento não declarado e vendas em falta permanecem explícitos ou indeterminados, sem substituir por zero. Deduções, créditos, reembolsos, regimes especiais, apuramento final/Modelo A completo e outros impostos não são implementados por este lote. Cobertura técnica não é uma data legal de revogação. |
Reutilizados os recibos versionados `integrated-review-continuation-20261020-receipt.json`, `treatment-scope-installation-receipt-20261023.json` e os fechos posteriores do manual. Alguns caminhos históricos `outputs/` não existem nesta cópia do workspace: essa ausência não revoga provas aceites nem autoriza regenerá-las. Nenhuma suite de aceitação anterior foi reexecutada nesta análise.
### Saldo concreto — duas lacunas de código e uma prova específica
| ID / situação | Ecrã, causa e efeito delimitado | Lote proposto e critério de fecho |
| --- | --- | --- |
| **S1 — lacuna de código confirmada por diagnóstico isolado novo** | **Tesouraria → vendas reutilizadas pelo IVA → relatório.** `CashflowDraftSnapshot` v1 guarda montantes/modo, mas não a moeda de origem. O painel e o relatório usam a moeda actual do projecto; o fallback `monthlySalesFromDraft` não tem moeda guardada para comparar. Serializar receita `123` enquanto o projecto está em USD e depois mudar o projecto para MZN produz uma entrada IVA `123 MZN` e relatório MZN, sem conservar USD. O plano empresarial já tem protecção própria de moeda; a lacuna é a tesouraria guardada, não todos os planeadores. | Versionar a moeda dos pressupostos de tesouraria e partilhar a guarda entre painel, IVA e relatório. Divergência exige reconciliação explícita, sem conversão/reclassificação silenciosa nem perda dos montantes. Snapshots v1 continuam legíveis e intactos; moeda sem origem recuperável fica por confirmar, nunca presumida como MZN. Prova nova só deste delta: USD → MZN, gravação/recuperação, ecrã/IVA/exportação coerentes; valores e edições conservados. Diagnóstico sem DB/rede em `outputs/simulator-scope-20261024/currency-gap.json`. |
| **S2 — lacuna identificada no código; prova de interface dirigida a fazer com a correcção** | **IVA mensal, execução já admitida ainda em fila/processamento.** Em `SimulationMonthlyIva`, o efeito regressa por `!available` antes de consultar o ID pendente e o botão também fica desactivado. A suspensão do consumidor, que deve impedir novas admissões, bloqueia assim a recuperação pela componente mensal. O servidor já preserva a versão admitida e permite consultar o histórico; não há falha de cálculo ou perda da execução demonstrada. | Separar nova admissão de consulta/retoma por ID. Execução admitida deve ser recuperável com consumidor suspenso, respeitando autenticação/dono, sem novo POST nem nova regra. Prova dirigida em QA: mesma execução pendente → indisponibilidade para novas admissões → conclusão/recuperação por mesmo ID → rascunho/relatório com o snapshot correcto e sem duplicação. Não repetir os testes aceites do avaliador ou publicar/suspender a regra real de homologação. |
| **S3 — ainda sem prova específica, não defeito presumido** | **Bloco mensal de IVA nos ficheiros finais.** Há prova de reutilização das vendas, snapshots mensais no servidor e composição do relatório, e provas aceites dos exportadores gerais. Não há neste inventário uma conferência explícita de ficheiros finais com o bloco mensal completo — valores exactos, estado actual/histórico, oportunidade, fonte e versão — incluindo a paginação desse bloco. Os PDFs anteriores cobriam outros deltas. | Conferir um cenário mensal representativo por ecrã e CSV/Word, e apenas a paginação do bloco mensal no PDF nativo final. Reutilizar execução/snapshot sintético aceite, se recuperável, sem recalcular para testar exportação. Comparar montantes exactos, zero/falta, contexto, fonte, versão e aviso de ausência de apuramento final. ZIP e restante paginação/rodapé reutilizam as provas aceites. Corrigir apenas divergência efectivamente observada; não presumir falha de exportador. |
**Proposta de execução:** fechar S1 e S2 juntos, pelos consumidores de pressupostos/snapshots; verificar apenas os seus deltas e executar S3 sobre a candidata resultante. Integrar e disponibilizar somente depois dessas provas, pelo Git existente e preservando alterações concorrentes; conferir a revisão e o delta instalado. Não migrar dados activos nem publicar outra regra por esta proposta. As guardas, mensagens e compatibilidade de snapshots devem ser tratadas no mesmo lote, sem criar motor, formulários ou aprovações novos.
**Dependências reais:** nenhuma identificada entre S1–S3 e as seis associações legadas ou os dois originais incompletos. As fontes da regra IVA já cadastrada e os recibos fiscais existentes são reutilizados. Declaração Modelo A completa, outros impostos, elegibilidade/incentivos novos e extensão temporal não entram neste fecho. O resultado esperado é o ciclo **dados → cálculo disponível → gravação → recuperação → relatório** coerente neste âmbito, não a certificação de um simulador fiscal integral. A proposta está registada nas linhas existentes `AUD-SIM-01` e `AUD-SIM-INT02`–`08`; as notas e estimativas históricas ficam subordinadas a este saldo de três itens, sem outro backlog.
## Fecho do lote de estabilização e sequência de referência — 24/10/2026
**Ordem expressa da Tempo:** (1) correcções e estabilização; (2) simulador fiscal com âmbito definido; (3) Imobiliário; (4) Bens de consumo e franquias; (5) Estatística e painéis; (6) IA. A preservação técnica é responsabilidade do Codex e dos programadores. A Tempo avalia utilidade, clareza e apresentação; não tem de redescobrir regressões para comprovar preservação.
@@ -24,6 +78,8 @@ Quando se alterar acesso, hierarquia ou serviço de mapas, executar o delta corr
**Saldo de fecho:** zero falhas técnicas conhecidas abertas neste lote; limites documentais, itens sem prova e desenvolvimento futuro permanecem identificados acima e nas mesmas linhas da matriz. Não se declara a aplicação transversal integralmente concluída. O próximo lote é a delimitação do simulador fiscal; esta passagem não activa cálculos, fontes, estatísticas ou IA. A avaliação de navegação/apresentação da Tempo pode continuar pelo percurso normal e alimenta apenas falhas concretamente observadas.
**Referência consolidada instalada: `276d833a4b4f9071db328b653f48529b2dfac4f9`**, confirmada pelo `/build/revision.json` público. Etiqueta Git `reference/stabilization-20261024`. A comparação Git de `app`, `resources`, `routes`, `database`, `config`, `public` e `deploy` com `066707ec` é vazia; não há novo delta funcional a reensaiar. Os recibos aceites conservam os mesmos bytes canónicos. Mantidas 3 423 linhas lógicas e 49 colunas da matriz; só nove linhas existentes foram reconciliadas. A prova anterior de preservação das 29 fichas, 28 relações e 408 geometrias é reutilizada, não apresentada como nova consulta autenticada da base nesta passagem. [Recibo da referência](stabilization-reference-receipt-20261024.json).
## Mapas — estabilização comum e divulgação delimitada, 24/10/2026
Diagnóstico por leitura autenticada na homologação `cf2ddf21`: **29 fichas, 28 relações territoriais e 408 geometrias válidas**, sem eliminação. Os 408 limites INE/2019 conservam classificação interna, propósito administrativo e SRID 4326. O fingerprint integral das linhas é `7d8b71d3e65d89ffdc43c748b0572dbc`. Não há RLS nas tabelas comparadas. O serviço comum não mudou desde `30aed7cd`: os consumidores gerais pediam exclusivamente geometrias públicas; o cadastro usava ainda `manage-portal`, ausente nos dois superutilizadores aprovados. A prova anterior demonstrava o cadastro privado, não a divulgação pública. O mapa base, tiles e assets carregam; a resposta territorial vazia explica a ausência de contornos e de enquadramento do território escolhido.
File diff suppressed because one or more lines are too long
@@ -5,6 +5,7 @@ import {
defaultCashflowDraftInputs,
restoreCashflowDraftSnapshot,
serializeCashflowDraftSnapshot,
cashflowCurrency,
type CashflowDraftSnapshot,
} from '@/lib/monthly-cashflow';
import {
@@ -24,6 +25,9 @@ export function SimulationCashflowPlanner({
onSnapshotChange?: (snapshot: string) => void;
}) {
const restored = restoreCashflowDraftSnapshot(snapshot);
const [confirmedCurrency, setConfirmedCurrency] = useState('');
const originCurrency = restored?.version === 2 ? restored.currency : null;
const calculationCurrency = snapshot ? originCurrency ?? '' : currency;
const [taxMode, setTaxMode] = useState<'fixed' | 'schedule'>(
restored?.taxMode ?? 'fixed',
);
@@ -41,7 +45,7 @@ export function SimulationCashflowPlanner({
) {
setInputs(next);
setTaxMode(mode);
onSnapshotChange?.(serializeCashflowDraftSnapshot(mode, next));
onSnapshotChange?.(serializeCashflowDraftSnapshot(mode, next, snapshot ? originCurrency ?? null : currency || null));
}
const set = (
key: Exclude<keyof typeof inputs, 'monthlyTaxSchedule'>,
@@ -63,7 +67,8 @@ export function SimulationCashflowPlanner({
throw new Error(
'Não foi possível recuperar os pressupostos guardados de tesouraria. Reveja os campos antes de voltar a guardar; nenhum resultado foi calculado com dados alternativos.',
);
projection = projectMonthlyCashflow({ ...cashflowInput, currency });
if (restored) cashflowCurrency(restored, currency);
projection = projectMonthlyCashflow({ ...cashflowInput, currency: calculationCurrency });
} catch (cause) {
error =
cause instanceof Error
@@ -76,7 +81,7 @@ export function SimulationCashflowPlanner({
aria-labelledby="cashflow-title"
>
<label className="block max-w-xs text-sm font-medium">
Moeda dos valores (código ISO)
Moeda do projecto (código ISO)
<input
list="cashflow-currency-options"
inputMode="text"
@@ -103,6 +108,16 @@ export function SimulationCashflowPlanner({
<h3 id="cashflow-title" className="text-lg font-semibold">
Projecção de tesouraria — 12 meses
</h3>
{restored && !originCurrency && <div className="space-y-2 rounded border border-amber-600 p-3">
<p>A moeda de origem dos montantes guardados não foi registada. Os valores foram conservados. Confirme apenas essa moeda; não é necessário preencher os montantes novamente.</p>
<label>Moeda de origem da tesouraria guardada<select className="ml-2 rounded border p-2" value={confirmedCurrency} onChange={event => setConfirmedCurrency(event.target.value)}><option value="">Por confirmar</option>{simulationCurrencyOptions.map(code => <option key={code} value={code}>{code}</option>)}</select></label>
<button type="button" className="block underline" disabled={!isSimulationCurrency(confirmedCurrency)} onClick={() => onSnapshotChange?.(serializeCashflowDraftSnapshot(taxMode, inputs, confirmedCurrency))}>Confirmar moeda de origem e conservar montantes</button>
</div>}
{originCurrency && originCurrency !== currency && <div role="alert" className="rounded border border-amber-600 p-3">
<p>{cashflowCurrency(restored!, currency).notice}</p>
<button type="button" className="underline" onClick={() => onCurrencyChange(originCurrency)}>Usar {originCurrency} no projecto e conservar montantes</button>
</div>}
{originCurrency && <p>Moeda de origem dos pressupostos: {originCurrency}.</p>}
<p className="text-sm text-slate-700">
Introduza pressupostos na mesma moeda. O imposto indicado é um
pressuposto do utilizador; esta ferramenta não calcula nem
@@ -257,12 +272,12 @@ export function SimulationCashflowPlanner({
{error}
</p>
)}
{projection && isSimulationCurrency(currency) && (
{projection && isSimulationCurrency(calculationCurrency) && (
<>
<p aria-live="polite" className="font-semibold">
{projection.endingCash === null
? `Caixa conhecida antes de imposto em falta, ao fim de 12 meses (${currency}):`
: `Saldo projectado ao fim de 12 meses (${currency}):`}{' '}
? `Caixa conhecida antes de imposto em falta, ao fim de 12 meses (${calculationCurrency}):`
: `Saldo projectado ao fim de 12 meses (${calculationCurrency}):`}{' '}
{projection.endingCash ??
projection.endingCashBeforeUnassessedTax}
</p>
@@ -274,7 +289,7 @@ export function SimulationCashflowPlanner({
[
exportMonthlyCashflowCsv({
...cashflowInput,
currency,
currency: calculationCurrency,
periods: 12,
}),
],
@@ -1,8 +1,8 @@
import { InvestmentContextAssessment } from './investment-context-assessment';
import { useEffect, useRef, useState } from 'react';
import { monthlyIvaInput, monthlyIvaKey, monthlySalesFromDraft, readMonthlyIvaRecord, readMonthlyIvaSettings, monthlyIvaCashTreatment, type MonthlyIvaInput, type MonthlyIvaSettings } from '@/lib/monthly-iva';
import { monthlyIvaInput, monthlyIvaKey, monthlySalesFromDraft, readMonthlyIvaRecord, readMonthlyIvaSettings, recoverMonthlyIvaRecord, monthlyIvaCashTreatment, type MonthlyIvaInput, type MonthlyIvaSettings } from '@/lib/monthly-iva';
import { fiscalQaRows } from '@/lib/fiscal-qa-preview';
import { isApiSimulationReadResponseV1, type ApiSimulationExecutionV1 } from '@/lib/simulation-contract';
import { type ApiSimulationExecutionV1 } from '@/lib/simulation-contract';
import { SimulationModeloA } from './simulation-modelo-a';
export function SimulationMonthlyIva({draft, busy, onCalculate, onChange}: {
@@ -16,6 +16,9 @@ export function SimulationMonthlyIva({draft, busy, onCalculate, onChange}: {
const [error, setError] = useState('');
const [available, setAvailable] = useState<boolean | null>(null);
const attempt = useRef('');
const mounted = useRef(true);
const latestSnapshot = useRef(draft.monthly_iva_snapshot); latestSnapshot.current = draft.monthly_iva_snapshot;
useEffect(() => { mounted.current = true; return () => { mounted.current = false; }; }, []);
const callbacks = useRef({onCalculate, onChange}); callbacks.current = {onCalculate, onChange};
let input: MonthlyIvaInput | null = null; let inputError = ''; let source = '';
try { source = monthlySalesFromDraft(draft).source; input = monthlyIvaInput(draft, settings); }
@@ -23,6 +26,8 @@ export function SimulationMonthlyIva({draft, busy, onCalculate, onChange}: {
const key = input ? monthlyIvaKey(input) : '';
const current = !!(input && record && monthlyIvaKey(record.input) === key);
const completed = current && record?.execution.status === 'completed';
const pendingRecord = record && ['queued','running'].includes(record.execution.status) ? record : null;
const actionKey = pendingRecord ? `recover:${pendingRecord.execution.id}` : key;
const saveSettings = (patch: Partial<MonthlyIvaSettings>) => onChange('monthly_iva_settings', JSON.stringify({...settings, ...patch}));
useEffect(() => {
let active = true;
@@ -32,27 +37,26 @@ export function SimulationMonthlyIva({draft, busy, onCalculate, onChange}: {
return () => { active = false; };
}, [retry]);
useEffect(() => {
if (!settings.auto || !input || input.currency !== 'MZN' || busy || !available || completed || attempt.current === key) return;
const request = input;
const pending = current && record && ['queued','running'].includes(record.execution.status) ? record.execution.id : null;
if (!settings.auto || busy || attempt.current === actionKey) return;
if (!pendingRecord && (!input || input.currency !== 'MZN' || !available || completed)) return;
const request = pendingRecord?.input ?? input!;
const savedSnapshot = draft.monthly_iva_snapshot;
const timer = window.setTimeout(() => {
attempt.current = key; setError('');
attempt.current = actionKey; setError('');
void (async () => {
try {
let execution;
if (pending) {
const response = await fetch(`/api/v1/simulations/${pending}`, {credentials:'same-origin', headers:{Accept:'application/json'}});
const value: unknown = await response.json();
if (!response.ok || !isApiSimulationReadResponseV1(value)) throw new Error('Não foi possível recuperar a execução mensal guardada. Não foi criada uma execução alternativa.');
execution = value.data;
if (pendingRecord) {
execution = (await recoverMonthlyIvaRecord(pendingRecord)).execution;
} else execution = await callbacks.current.onCalculate(request);
if (!execution) throw new Error('O cálculo mensal não foi concluído. As vendas e a versão anterior foram preservadas; tente novamente.');
if (!mounted.current || (pendingRecord && latestSnapshot.current !== savedSnapshot)) return;
callbacks.current.onChange('monthly_iva_snapshot', JSON.stringify({input:request, execution}));
} catch (cause) {setError(cause instanceof Error ? cause.message : 'Não foi possível calcular o IVA mensal.');}
} catch (cause) {if (mounted.current) setError(cause instanceof Error ? cause.message : 'Não foi possível calcular o IVA mensal.');}
})();
}, 700);
return () => window.clearTimeout(timer);
}, [key, settings.auto, busy, available, completed, retry]);
}, [actionKey, key, settings.auto, busy, available, completed, retry]);
const snapshot = record?.execution.result_snapshot;
const projection = snapshot?.monthly_projection as {rows?: Array<{date:string; sales_declared:string|null; net_sales:string|null; rate:string|null; vat:string|null; gross_sales:string|null; status:string}>} | undefined;
@@ -73,7 +77,8 @@ export function SimulationMonthlyIva({draft, busy, onCalculate, onChange}: {
{settings.auto && !completed && current && <p role="status">Execução mensal guardada: {record?.execution.status === 'queued' ? 'em fila' : record?.execution.status === 'running' ? 'em processamento' : 'não concluída'}. Pode voltar a consultar sem duplicar.</p>}
{busy && <p role="status">A actualizar os resultados dependentes das vendas…</p>}
{error && <p role="alert">{error}</p>}
<button type="button" disabled={busy || !available || !input || input.currency !== 'MZN'} onClick={() => {attempt.current=''; setRetry((value) => value+1); saveSettings({auto:true});}} className="rounded bg-emerald-800 p-2 text-white disabled:opacity-50">Calcular / actualizar IVA mensal e guardar versão</button>
<button type="button" disabled={busy || (!pendingRecord && (!available || !input || input.currency !== 'MZN'))} onClick={() => {attempt.current=''; setRetry((value) => value+1); saveSettings({auto:true});}} className="rounded bg-emerald-800 p-2 text-white disabled:opacity-50">{pendingRecord ? 'Recuperar execução mensal admitida pelo mesmo ID' : 'Calcular / actualizar IVA mensal e guardar versão'}</button>
{pendingRecord && <p>Consulta da execução {pendingRecord.execution.id}. A suspensão de novos cálculos não impede esta recuperação; não é criado outro pedido.</p>}
{settings.auto && <button type="button" className="ml-3 underline" onClick={() => saveSettings({auto:false})}>Pausar actualização automática</button>}
{input && <p className="text-sm">{input.monthly_sales.length} meses; moeda {input.currency || 'por indicar'}. Campo vazio significa informação em falta; zero declarado permanece zero. Depois da primeira execução, alterações das vendas actualizam o cálculo enquanto a actualização automática estiver ligada.</p>}
{projection?.rows && <div className="overflow-x-auto" tabIndex={0}><table className="min-w-full text-sm"><caption>IVA mensal — {current ? 'versão correspondente às entradas' : 'versão histórica'}</caption><thead><tr>{['Mês','Vendas declaradas','Vendas tributáveis sem IVA','Taxa aplicada (%)','IVA calculado','Total com IVA','Estado'].map((label) => <th key={label} className="p-2 text-left">{label}</th>)}</tr></thead><tbody>{projection.rows.map((row) => <tr key={row.date}><th scope="row" className="p-2">{row.date.slice(0,7)}</th><td>{row.sales_declared ?? 'Sem informação'}</td><td>{row.net_sales ?? 'Indeterminado'}</td><td>{row.rate ?? 'Indeterminada'}</td><td>{row.vat ?? 'Indeterminado'}</td><td>{row.gross_sales ?? 'Indeterminado'}</td><td>{{calculated:'Calculado',missing_sales:'Sem informação de vendas',unsupported_basis:'Base líquida não determinada',indeterminate:'Enquadramento ou regra indeterminados'}[row.status] ?? 'Indeterminado'}</td></tr>)}</tbody></table></div>}
+15 -4
View File
@@ -17,7 +17,9 @@ export function defaultCashflowDraftInputs() {
}
export type CashflowDraftSnapshot = {
version: 1;
version: 1 | 2;
/** Missing in legacy v1: never infer it from the current project. */
currency?: string | null;
taxMode: 'fixed' | 'schedule';
inputs: ReturnType<typeof defaultCashflowDraftInputs>;
};
@@ -31,7 +33,8 @@ export function restoreCashflowDraftSnapshot(
const parsed = JSON.parse(snapshot);
if (
!parsed ||
parsed.version !== 1 ||
![1, 2].includes(parsed.version) ||
(parsed.version === 2 && parsed.currency !== null && (typeof parsed.currency !== 'string' || !/^[A-Z]{3}$/.test(parsed.currency))) ||
!['fixed', 'schedule'].includes(parsed.taxMode) ||
!parsed.inputs ||
typeof parsed.inputs !== 'object' ||
@@ -55,7 +58,7 @@ export function restoreCashflowDraftSnapshot(
} else if (typeof value !== 'string' || value.length > 160)
return null;
}
return { version: 1, taxMode: parsed.taxMode, inputs: parsed.inputs };
return { version: parsed.version, ...(parsed.version === 2 ? {currency: parsed.currency} : {}), taxMode: parsed.taxMode, inputs: parsed.inputs };
} catch {
return null;
}
@@ -64,8 +67,16 @@ export function restoreCashflowDraftSnapshot(
export function serializeCashflowDraftSnapshot(
taxMode: CashflowDraftSnapshot['taxMode'],
inputs: CashflowDraftSnapshot['inputs'],
currency?: string | null,
): string {
return JSON.stringify({ version: 1, taxMode, inputs });
return JSON.stringify({ version: currency === undefined ? 1 : 2, ...(currency !== undefined ? {currency} : {}), taxMode, inputs });
}
export function cashflowCurrency(saved: CashflowDraftSnapshot, projectCurrency: string): {currency: string; matchesProject: boolean; notice: string} {
const currency = saved.version === 2 ? saved.currency : null;
if (!currency) throw new Error('A moeda de origem da tesouraria guardada está por confirmar. Os montantes foram preservados; confirme a moeda na ferramenta de tesouraria antes de calcular ou reutilizar as vendas.');
const matchesProject = currency === projectCurrency;
return {currency, matchesProject, notice: matchesProject ? '' : `A tesouraria foi guardada em ${currency}, mas o projecto está em ${projectCurrency || 'moeda por indicar'}. Os montantes continuam em ${currency}; não foram convertidos nem reinterpretados. Para os reutilizar no projecto, restaure explicitamente a moeda de origem. Uma projecção noutra moeda exige pressupostos próprios.`};
}
export type MonthlyCashflowInput = {
+10 -1
View File
@@ -1,5 +1,5 @@
import { monthlyProjectedSales, restoreBusinessPlanSnapshot } from './business-financial-plan.ts';
import { restoreCashflowDraftSnapshot } from './monthly-cashflow.ts';
import { cashflowCurrency, restoreCashflowDraftSnapshot } from './monthly-cashflow.ts';
import { isApiSimulationReadResponseV1, type ApiSimulationExecutionV1 } from './simulation-contract.ts';
export type MonthlyIvaInput = {
@@ -27,6 +27,8 @@ export function monthlySalesFromDraft(draft: Record<string, unknown>): { amounts
if (draft.cashflow_snapshot) {
const saved = restoreCashflowDraftSnapshot(String(draft.cashflow_snapshot));
if (!saved) throw new Error('Não foi possível recuperar a receita projectada.');
const origin = cashflowCurrency(saved, String(draft.currency ?? ''));
if (!origin.matchesProject) throw new Error(origin.notice);
return {amounts: Array.from({length: 12}, () => saved.inputs.monthlyRevenue.trim() === '' ? null : saved.inputs.monthlyRevenue), source: 'Receita mensal constante já declarada na projecção de tesouraria; confirme que corresponde a vendas tributáveis.'};
}
return {amounts: Array.from({length: 12}, () => null), source: 'Sem projecção de vendas guardada. Prepare as vendas no plano empresarial; não é necessário voltar a introduzi-las aqui.'};
@@ -48,4 +50,11 @@ export function readMonthlyIvaRecord(raw?: string): MonthlyIvaRecord | null {
} catch { /* Keep the stored value intact; no fallback fiscal result. */ }
return null;
}
/** Read an admitted execution through the existing ownership-protected endpoint; never create one. */
export async function recoverMonthlyIvaRecord(record: MonthlyIvaRecord, request: typeof fetch = fetch): Promise<MonthlyIvaRecord> {
const response = await request(`/api/v1/simulations/${encodeURIComponent(record.execution.id)}`, {credentials:'same-origin', headers:{Accept:'application/json'}});
const value: unknown = await response.json();
if (!response.ok || !isApiSimulationReadResponseV1(value) || value.data.id !== record.execution.id) throw new Error('Não foi possível recuperar a execução mensal guardada. Os dados foram preservados e não foi criada uma execução alternativa.');
return {input: record.input, execution: value.data};
}
export const monthlyIvaCashTreatment = 'IVA apresentado separadamente: não é somado novamente aos recebimentos declarados nem deduzido como custo ou imposto final a pagar. A tesouraria conserva os recebimentos e pagamentos introduzidos, antes de uma liquidação fiscal não apurada; não se presume cobrança integral das vendas no mesmo mês.';
+4 -1
View File
@@ -7,6 +7,7 @@ import { fiscalQaRows } from './fiscal-qa-preview.ts';
import { csvCellWithDelimiter } from './csv.js';
import {
restoreCashflowDraftSnapshot,
cashflowCurrency,
projectMonthlyCashflow,
} from './monthly-cashflow.ts';
import {
@@ -637,7 +638,9 @@ export function buildProjectDossierRows(
throw new Error(
'Pressupostos de tesouraria não recuperáveis; não foram substituídos por valores alternativos.',
);
const currency = String(draft.currency ?? '');
const originCurrency = cashflowCurrency(saved, String(draft.currency ?? ''));
const currency = originCurrency.currency;
if (originCurrency.notice) rows.push(['Tesouraria', 'Divergência de moeda', originCurrency.notice, 'Moeda original preservada; sem conversão']);
if (!currency)
throw new Error(
'Indique uma moeda para interpretar os montantes de tesouraria.',
@@ -0,0 +1,67 @@
<?php
namespace Tests\Feature;
use App\Jobs\RunSimulation;
use App\Models\Simulation;
use App\Models\SimulationTaxRuleVersion;
use App\Models\User;
use App\Services\SimulationExecution;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Queue;
use Illuminate\Support\Str;
use Tests\TestCase;
final class SimulatorClosureRecoveryTest extends TestCase
{
use DatabaseTransactions;
public function test_existing_monthly_admission_can_be_read_and_finished_when_new_admissions_are_off(): void
{
$identity = DB::selectOne('select current_database() as name,inet_server_port() as port');
$this->assertSame('simulator_fiscal_delta_20261021_test', $identity->name);
$this->assertSame(55441, (int) $identity->port);
Queue::fake();
// Reuse the existing isolated synthetic rule. No create/publish/suspend of any rule.
$rule = SimulationTaxRuleVersion::findOrFail('01a15e50-7972-72e0-8991-82ca28df9b49');
$before = $this->ruleFingerprint();
config(['simulation-fiscal-consumer.enabled' => true, 'simulation-fiscal-consumer.rule_id' => $rule->id, 'simulation-fiscal-consumer.rule_version' => $rule->rule_version]);
$actor = User::factory()->create(['email_verified_at' => now()]);
$input = ['scenario_kind' => 'fiscal_rule_published', 'currency' => 'MZN', 'scope_declared' => true, 'sales_vat_basis' => 'net', 'monthly_sales' => [['date' => '2026-01-01', 'amount' => '1000.03']]];
$id = $this->actingAs($actor)->postJson('/api/v1/simulations', $input, ['Idempotency-Key' => (string) Str::uuid()])->assertAccepted()->json('data.id');
$count = Simulation::count();
config(['simulation-fiscal-consumer.enabled' => false]);
$this->getJson('/api/v1/fiscal-consumer')->assertOk()->assertJsonPath('data.enabled', false);
$this->getJson('/api/v1/simulations/'.$id)->assertOk()->assertJsonPath('data.id', $id)->assertJsonPath('data.status', 'queued');
$this->postJson('/api/v1/simulations', $input, ['Idempotency-Key' => (string) Str::uuid()])->assertStatus(503);
(new RunSimulation($id, $actor->id, null))->handle(app(SimulationExecution::class));
$this->getJson('/api/v1/simulations/'.$id)->assertOk()->assertJsonPath('data.id', $id)->assertJsonPath('data.status', 'completed');
$this->assertSame($count, Simulation::count());
$this->actingAs(User::factory()->create(['email_verified_at' => now()]))->getJson('/api/v1/simulations/'.$id)->assertNotFound();
$this->assertSame($before, $this->ruleFingerprint());
}
private function ruleFingerprint(): string
{
return DB::selectOne("select md5(string_agg(row_to_json(r)::text,E'\\n' order by id)) as hash from simulation_tax_rule_versions r")->hash;
}
public function test_cashflow_original_currency_and_amounts_round_trip_in_the_existing_private_draft(): void
{
$identity = DB::selectOne('select current_database() as name,inet_server_port() as port');
$this->assertSame('simulator_fiscal_delta_20261021_test', $identity->name);
$this->assertSame(55441, (int) $identity->port);
config(['investor-path.enabled' => true, 'suitecrm.enabled' => false]);
$actor = User::factory()->create(['email_verified_at' => now()]);
$this->actingAs($actor);
$id = $this->postJson('/investimentos', ['title' => 'SYNTHETIC QA — moeda de tesouraria', 'client_key' => (string) Str::uuid(), 'reuse_consent' => true])->assertCreated()->json('id');
$snapshot = json_encode(['version' => 2, 'currency' => 'USD', 'taxMode' => 'fixed', 'inputs' => ['monthlyRevenue' => '123', 'monthlyTaxSchedule' => ['0', '', '0']]], JSON_THROW_ON_ERROR);
$this->postJson('/api/v1/simulation-drafts', ['context' => $id, 'personal_snapshot' => [], 'economic_snapshot' => ['purpose' => 'simulate_only', 'project_title' => 'SYNTHETIC QA — moeda', 'currency' => 'MZN', 'cashflow_snapshot' => $snapshot]], ['Idempotency-Key' => (string) Str::uuid()])->assertCreated();
$this->actingAs($actor->fresh());
$saved = $this->getJson('/api/v1/simulation-drafts/current?context='.$id)->assertOk()->json('data.economic_snapshot');
$this->assertSame('MZN', $saved['currency']);
$this->assertSame($snapshot, $saved['cashflow_snapshot']);
$this->assertSame('USD', json_decode($saved['cashflow_snapshot'], true)['currency']);
}
}
@@ -0,0 +1,13 @@
import {serializeBusinessPlanSnapshot} from '../../../resources/js/lib/business-financial-plan.ts';
export function closureFixture(status='completed') {
const fields=['openingCash','monthlyCollections','monthlyVariableCostPayments','monthlyInventoryPurchases','monthlyOperatingExpenses','monthlyOperatingPayments','monthlyPayrollExpense','monthlyPayrollPayments','assetInvestment','inventory','receivables','payables','equity','shareholderLoans','bankLoanProceeds','grants','assetDepreciation','monthlyInterestExpense','monthlyInterestPayment','monthlyPrincipalRepayment'];
const plan={...Object.fromEntries(fields.map(k=>[k,'0'])),currency:'MZN',periods:4,depreciationLifeMonths:60,products:[{label:'SYNTHETIC QA',monthlyUnits:'1',unitPrice:'1000.03',unitVariableCost:'0',monthlyUnitsByPeriod:['1','2','0','']}]};
const input={scenario_kind:'fiscal_rule_published',monthly_sales:[{date:'2026-01-01',amount:'1000.03'},{date:'2026-02-01',amount:'2000.06'},{date:'2026-03-01',amount:'0.00'},{date:'2026-04-01',amount:null}],sales_vat_basis:'net',currency:'MZN',scope_declared:true};
// Frozen numbers already accepted in the monthly QA receipt; no tax calculation here.
const rows=[{date:'2026-01-01',sales_declared:'1000.03',net_sales:'1000.03',rate:'16',vat:'160.0048',gross_sales:'1160.0348',status:'calculated'},{date:'2026-02-01',sales_declared:'2000.06',net_sales:'2000.06',rate:'16',vat:'320.0096',gross_sales:'2320.0696',status:'calculated'},{date:'2026-03-01',sales_declared:'0.00',net_sales:'0.00',rate:'16',vat:'0',gross_sales:'0',status:'calculated'},{date:'2026-04-01',sales_declared:null,net_sales:null,rate:null,vat:null,gross_sales:null,status:'missing_sales'}];
const snapshot={contract_version:1,scenario_kind:'fiscal_rule_published',base:null,currency:'MZN',indicative_total:null,legal_interpretation:false,formula_version:'investmoz.tax.v1',context:{reference_date:'2026-01-01',currency:'MZN',currency_fraction_digits:2,scenario_id:'61000100-0000-4000-8000-000000000099',scenario_version:1,engine_version:'fiscal-published-v1',ruleset_version:'IVA-GERAL-BASE-LIQUIDA:4'},result:{status:'indeterminate',value:null,unit:'MZN',explanation:'IVA das vendas projectadas; sem apuramento final.',missing_inputs:['monthly_sales'],source_ids:['synthetic-qa-source']},presentation:{value:null,rounding_applied:false},fiscal_provenance:{rule:{rule_code:'IVA-GERAL-BASE-LIQUIDA',rule_version:4,valid_from:'2023-01-01',valid_until:null,source_sha256:'synthetic-qa-source-hash',source:{diploma:'Fonte sintética QA — regra já ensaiada',article:'Cópia do snapshot aceite'}},scope_status:'user_declared'},monthly_projection:{input,rows,cash_treatment:'IVA separado da tesouraria; não é imposto final a pagar.'}};
const execution={id:'06566910-9672-4176-804d-aad8350417cb',status,version_no:2,result_snapshot:status==='completed'?snapshot:null};
const context={version:1,context_revision:13,consumer_opportunity:'opportunity:309',opportunity_choices:[{opportunity:{key:'opportunity:309',title:'SYNTHETIC QA — segunda oportunidade Manica'}}],path:{steps:[]}};
return {input,snapshot,execution,draft:{project_title:'SYNTHETIC QA — fecho do simulador S1 S2 S3',currency:'MZN',context_opportunity:'opportunity:309',context_applicability_snapshot:JSON.stringify(context),business_plan_snapshot:serializeBusinessPlanSnapshot(plan),monthly_iva_settings:JSON.stringify({start:'2026-01',basis:'net',declared:true,auto:false}),monthly_iva_snapshot:JSON.stringify({input,execution})}};
}
@@ -0,0 +1,55 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import {mkdir,writeFile} from 'node:fs/promises';
import JSZip from 'jszip';
import {defaultCashflowDraftInputs,serializeCashflowDraftSnapshot,restoreCashflowDraftSnapshot,cashflowCurrency} from '../../resources/js/lib/monthly-cashflow.ts';
import {monthlyIvaInput,recoverMonthlyIvaRecord} from '../../resources/js/lib/monthly-iva.ts';
import {exportProjectDossierCsv,buildProjectDossierRows} from '../../resources/js/lib/project-dossier.ts';
import {createProjectDossierReport,generateProjectDossierDocx,createProjectDossierPrintHtml} from '../../resources/js/lib/project-dossier-report.ts';
import {closureFixture} from './fixtures/simulator-closure-fixture.mjs';
test('S1 saved USD stays USD in report and cannot be reused as MZN for IVA',()=>{
const inputs={...defaultCashflowDraftInputs(),monthlyRevenue:'123',monthlyTaxAssessed:'0'};
const snapshot=serializeCashflowDraftSnapshot('fixed',inputs,'USD');
const draft={currency:'MZN',cashflow_snapshot:snapshot};
const saved=restoreCashflowDraftSnapshot(snapshot);
assert.equal(saved.currency,'USD');assert.deepEqual(saved.inputs,inputs);
assert.equal(cashflowCurrency(saved,'MZN').matchesProject,false);
assert.throws(()=>monthlyIvaInput(draft,{start:'2026-01',basis:'net',declared:true,auto:false}),/continuam em USD/);
const rows=buildProjectDossierRows(draft).filter(r=>r[0]==='Tesouraria');
assert.match(rows.find(r=>r[1]==='Pressupostos e período')[2],/moeda USD/);
assert.match(rows.find(r=>r[1]==='Divergência de moeda')[2],/não foram convertidos/);
const restoredProject={...draft,currency:'USD'};
assert.equal(cashflowCurrency(restoreCashflowDraftSnapshot(restoredProject.cashflow_snapshot),restoredProject.currency).matchesProject,true);
assert.equal(restoredProject.cashflow_snapshot,snapshot);
});
test('S1 legacy amounts remain readable but require explicit origin currency before reuse',()=>{
const inputs={...defaultCashflowDraftInputs(),monthlyRevenue:'123'};
const legacy=serializeCashflowDraftSnapshot('fixed',inputs);const saved=restoreCashflowDraftSnapshot(legacy);
assert.equal(saved.version,1);assert.throws(()=>cashflowCurrency(saved,'MZN'),/por confirmar/);
const confirmed=restoreCashflowDraftSnapshot(serializeCashflowDraftSnapshot(saved.taxMode,saved.inputs,'USD'));
assert.deepEqual(confirmed.inputs,saved.inputs);assert.equal(confirmed.currency,'USD');
assert.equal(restoreCashflowDraftSnapshot(legacy).version,1);
});
test('S2 recovery is a same-ID authenticated GET without new admission and preserves denied records',async()=>{
const pending=closureFixture('queued');const completed=closureFixture();const calls=[];
const record={input:pending.input,execution:pending.execution};const before=JSON.stringify(record);
const request=async(url,options)=>{calls.push({url,options});return new Response(JSON.stringify({data:completed.execution}),{status:200});};
const recovered=await recoverMonthlyIvaRecord(record,request);
assert.equal(recovered.execution.id,pending.execution.id);assert.equal(recovered.execution.status,'completed');assert.deepEqual(recovered.input,pending.input);
assert.equal(calls.length,1);assert.equal(calls[0].options.credentials,'same-origin');assert.equal(calls[0].options.method,undefined);assert.match(calls[0].url,/simulations\/06566910/);
await assert.rejects(()=>recoverMonthlyIvaRecord(record,async()=>new Response('{}',{status:403})),/preservados/);
await assert.rejects(()=>recoverMonthlyIvaRecord(record,async()=>new Response(JSON.stringify({data:{...completed.execution,id:'00000000-0000-4000-8000-000000000000'}}),{status:200})),/preservados/);
assert.equal(JSON.stringify(record),before);
});
test('S3 final CSV Word and print source retain frozen monthly values context source and version',async()=>{
const {draft}=closureFixture();
const report=createProjectDossierReport(draft,{selectedSections:new Set(['sources','finance']),version:'SYNTHETIC QA S3'});
const csv=exportProjectDossierCsv(draft);const html=await createProjectDossierPrintHtml(report,[],false);
const docx=await generateProjectDossierDocx(report,[],true);const bytes=new Uint8Array(await docx.arrayBuffer());const word=await JSZip.loadAsync(bytes);const xml=await word.file('word/document.xml').async('string');
for(const text of [csv,html,xml]) for(const expected of ['160.0048','320.0096','1160.0348','MZN','2026-01','2026-04','Fonte sintética QA','IVA-GERAL-BASE-LIQUIDA','segunda oportunidade Manica','opportunity:309']) assert.ok(text.includes(expected),expected);
for(const text of [csv,html,xml]) assert.match(text,/IVA-GERAL-BASE-LIQUIDA[^\n<]{0,30}4/);
assert.match(html,/segunda oportunidade Manica/);assert.match(xml,/segunda oportunidade Manica/);
const out='outputs/simulator-closure-qa-20261024';await mkdir(out,{recursive:true});
await Promise.all([writeFile(`${out}/monthly-final.csv`,csv),writeFile(`${out}/monthly-final.docx`,bytes),writeFile(`${out}/monthly-final.html`,html),writeFile(`${out}/frozen-fixture.json`,JSON.stringify(draft,null,2))]);
});
@@ -17,7 +17,7 @@ test('cashflow draft recovery preserves unanswered months and invalid editable v
openingCash: '77.01',
monthlyTaxSchedule: ['10', '', '20', ...Array(9).fill('0')],
};
const stored = serializeCashflowDraftSnapshot('schedule', inputs);
const stored = serializeCashflowDraftSnapshot('schedule', inputs, 'MZN');
const restored = restoreCashflowDraftSnapshot(stored);
assert.equal(restored.taxMode, 'schedule');
assert.deepEqual(restored.inputs, inputs);
@@ -65,7 +65,7 @@ test('cashflow draft recovery preserves unanswered months and invalid editable v
assert.equal(restoreCashflowDraftSnapshot('{"version":2}'), null);
assert.equal(
restoreCashflowDraftSnapshot(
stored.replace('"version":1', '"version":2'),
stored.replace('"version":2', '"version":3'),
),
null,
);