Compare commits

...
21 changed files with 1502 additions and 44 deletions
@@ -37,7 +37,7 @@ class StoreSimulationFormDraftRequest extends FormRequest
'contact_province', 'contact_district', 'preferred_channel',
];
$economic = [
'context_opportunity', 'context_applicability_snapshot', 'investment_regime', 'context_reuse', 'compatible_fields_snapshot', 'published_iva_base', 'published_iva_currency', 'published_iva_date', 'published_iva_scope', 'monthly_iva_settings', 'monthly_iva_snapshot', 'journey_type', 'detail_level', 'business_plan_snapshot', 'financial_tools_snapshot', 'cashflow_snapshot', 'project_team_snapshot', 'project_execution_snapshot', 'purpose', 'manifestation_type', 'preferred_language', 'project_title', 'project_summary', 'project_problem', 'target_customers', 'value_proposition', 'market_assumption', 'market_source', 'project_milestones', 'selection_labels', 'project_type',
'context_opportunity', 'context_applicability_snapshot', 'investment_regime', 'context_reuse', 'compatible_fields_snapshot', 'published_iva_base', 'published_iva_currency', 'published_iva_date', 'published_iva_scope', 'monthly_iva_settings', 'monthly_iva_snapshot', 'modelo_a_snapshot', 'journey_type', 'detail_level', 'business_plan_snapshot', 'financial_tools_snapshot', 'cashflow_snapshot', 'project_team_snapshot', 'project_execution_snapshot', 'purpose', 'manifestation_type', 'preferred_language', 'project_title', 'project_summary', 'project_problem', 'target_customers', 'value_proposition', 'market_assumption', 'market_source', 'project_milestones', 'selection_labels', 'project_type',
'competitor_assumption', 'sales_channel_assumption', 'marketing_approach', 'location_rationale', 'project_risk_responses_snapshot', 'project_questionnaire_version',
'customer_validation_status', 'cost_estimate_status', 'location_assessment_status', 'funding_status', 'receivables_timing_status',
'project_type_other', 'sector_id', 'activity_id', 'products_services', 'maturity_stage', 'expected_start_date',
@@ -83,6 +83,7 @@ class StoreSimulationFormDraftRequest extends FormRequest
'economic_snapshot.published_iva_scope' => ['nullable', 'string', 'in:yes,no'],
'economic_snapshot.monthly_iva_settings' => ['nullable', 'string', 'max:2000'],
'economic_snapshot.monthly_iva_snapshot' => ['nullable', 'string', 'max:60000'],
'economic_snapshot.modelo_a_snapshot' => ['nullable', 'string', 'json', 'max:1000000'],
'economic_snapshot.project_team_snapshot' => ['nullable', 'string', 'max:30000'],
'economic_snapshot.project_execution_snapshot' => ['nullable', 'string', 'max:30000'],
'economic_snapshot.project_risk_responses_snapshot' => ['nullable', 'string', 'max:30000'],
@@ -2001,5 +2001,730 @@
"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"
},
"modelo_a_next_scope_20261024": {
"status": "analysis and proposal only",
"preserved_closure": {
"revision": "cd10cccb",
"receipt": "355656b5",
"balance": 0
},
"inventory": [
{
"folder": "Notas",
"name": "Estamos a gerar um problema.docx",
"bytes": 28938,
"sha256": "aa589574f7e581ca0e41df1a690a60379062879fed04ce765b827b9965047e44",
"use": "inventory only"
},
{
"folder": "Notas",
"name": "INVESTEMOZ Ibane.docx",
"bytes": 144410,
"sha256": "c5252af6dfa38558b638fafcf726658bb10edabe92939a2f97f5862003f6bed9",
"use": "inventory only"
},
{
"folder": "Notas",
"name": "investimentos nos corredores.docx",
"bytes": 21595,
"sha256": "1473b06c48aa234e771660e3f2a51b9d2e4e1943bd52d878e0ae854ee6657055",
"use": "inventory only"
},
{
"folder": "Notas",
"name": "Neste momento.docx",
"bytes": 19639,
"sha256": "e6166411122a12089da1cb4436c6a2322338467dbd46ea4e57aee0015a49902c",
"use": "inventory only"
},
{
"folder": "Notas",
"name": "nota 1.docx",
"bytes": 25488,
"sha256": "7037ff6d968aa8f756a74adad76758b64ba54d786726aac9e0c7a54ecdc225da",
"use": "inventory only"
},
{
"folder": "Notas",
"name": "Novo Modelo A v1.docx",
"bytes": 532100,
"sha256": "63d45fa80db17a60b43619d55a25813229d670d14c758273f86316285db8e02b",
"use": "comparison with accepted reconciliation and current code",
"embedded_form_sha256": "7ae7f02efca96323087d0dfa5f3078b4ae724318edd5b6c4802c863d840bdf66"
},
{
"folder": "Notas",
"name": "Novo Modelo A.docx",
"bytes": 532008,
"sha256": "62c198e90d8c6792f7d5ccefc6386df2e4b6089573067d3a6e1eae7fc2b9572b",
"use": "comparison with accepted reconciliation and current code",
"embedded_form_sha256": "7ae7f02efca96323087d0dfa5f3078b4ae724318edd5b6c4802c863d840bdf66"
},
{
"folder": "Notas",
"name": "PROVÍNCIA DE INHAMBANE v1.docx",
"bytes": 109317,
"sha256": "32c6ab160c5028f3084d23b2173ce3cfe6dddba0715d1228da6bd9380247f486",
"use": "inventory only"
},
{
"folder": "Notas",
"name": "PROVÍNCIA DE INHAMBANE.docx",
"bytes": 108584,
"sha256": "9f628cd57eb860433f36058b299660834e9052d07e1f7ca99f68f1d1cfdc9d72",
"use": "inventory only"
},
{
"folder": "Notas",
"name": "~$OVÍNCIA DE INHAMBANE.docx",
"bytes": 162,
"sha256": "7c2c7731b5ef1023fa980c582051296bf2b5e75a8645113368c2aa9bf34cf12f",
"use": "temporary or lock file; not a source exemplar"
},
{
"folder": "Notas",
"name": "~$tamos a gerar um problema.docx",
"bytes": 162,
"sha256": "e726ad6723df9f664121d97e18854466b30efd24a1f8efab0a7945cbecf7059b",
"use": "temporary or lock file; not a source exemplar"
},
{
"folder": "Notas",
"name": "~$VESTEMOZ Ibane.docx",
"bytes": 162,
"sha256": "9e71bcb8ded90236b2ca70f10937b758eacc0e924cf89d46ba73f1540d6f3132",
"use": "temporary or lock file; not a source exemplar"
},
{
"folder": "Notas",
"name": "~$vestimentos nos corredores.docx",
"bytes": 162,
"sha256": "2b19e4a708c934710926f0dc5da74b785787d4818500e9c1d43359e06ffb9b57",
"use": "temporary or lock file; not a source exemplar"
},
{
"folder": "Notas",
"name": "~WRL2857.tmp",
"bytes": 143261,
"sha256": "85ed72087b6b670292e81c83a455a264a9c4cf2b36ee12807379d475358b3577",
"use": "temporary or lock file; not a source exemplar"
},
{
"folder": "Modelos",
"name": "Cópia de IRPS-IMOP.xlsx",
"bytes": 277846,
"sha256": "1ceca6f4c6d77853c1b2b445876599b42093175fa89eecf40590180956d83a9e",
"use": "inventory only"
},
{
"folder": "Modelos",
"name": "IRPC M-22- DECLARACAO DE RENDIMENTOS.pdf",
"bytes": 109727,
"sha256": "4b61bd83d1fbcfd3b7b5c5523cea5923f7d0f65dc4c004eff58e5db38f763c88",
"use": "inventory only"
},
{
"folder": "Modelos",
"name": "IRPC_IRPS.xlsx",
"bytes": 115701,
"sha256": "fd5ddb07392bcad1313a76ae85603169a56eeecd9ac6fa4a1be12cee909c6260",
"use": "inventory only"
},
{
"folder": "Modelos",
"name": "VAT Return.pdf",
"bytes": 285479,
"sha256": "9c0b99a810c7db53eb75d63c22ee22fb86a50ba57bd5509041ed80ee10f75741",
"use": "compared visually against both DOCX, 24/10; originals unchanged"
},
{
"folder": "Modelos",
"name": "~$IRPC_IRPS.xlsx",
"bytes": 165,
"sha256": "5a6f905aea475f25f054b0e13e4b2c8b955b7097789fcc3ca24ac2dd0b0bb284",
"use": "temporary or lock file; not a source exemplar"
}
],
"source_folders": "Tempo portal Notes and Modelos; originals read only, no import or publication",
"comparison": "docs/investmoz/simulations/modelo-a-reconciliacao-20261021.md",
"new_calculations_activated": false,
"irpc_irps": "inventoried, not implemented or validated as rules"
},
"modelo_a_acquisitions_qa_20261024": {
"status": "implemented and demonstrated in isolated QA; not integrated/deployed",
"base": "355656b5",
"preserved_homologation": "cd10cccb",
"preserved_receipt": "355656b5",
"stabilization_reference": "276d833a",
"scope": [
"domestic general MZN 2026 acquisitions08-10",
"conditional totals14-18",
"private opportunity-scoped draft and frozen report"
],
"document_comparison": {
"file": "VAT Return.pdf",
"sha256": "9c0b99a810c7db53eb75d63c22ee22fb86a50ba57bd5509041ed80ee10f75741",
"pages": 2,
"08_10_14_18": "same fields and formulas as both DOCX; no substantive difference identified in this scope",
"prefilled_values": "initial/within deadline and Maputo, not inherited",
"remaining_divergences": "regularization and credit numbering; refund article; only dependent future rules affected"
},
"proofs": {
"frontend_tests": 11,
"api_tests": 2,
"api_assertions": 19,
"database": "simulator_fiscal_delta_20261021_test:55441",
"qa_transactions": "rolled back",
"tax_rule_fingerprint": "unchanged",
"ui_second_field17": "128.0048",
"ui_first_field17_preserved": "144.0048",
"ui_to_api_report": "exact calculated payload downloaded from UI, saved/recovered through real private QA API; report without recalculation",
"types": "passed",
"build": "passed",
"pdf_pages": 6,
"pdf_visual": "all six reviewed, no clipping or overlap",
"files": {
"acquisitions-final.csv": {
"bytes": 15092,
"sha256": "3a988f8c097a0e0b8364f09b8451347679447f0cf0866485530881e229e3b511"
},
"acquisitions-final.docx": {
"bytes": 14283,
"sha256": "3aa9a9e45adaaae86368b2aa39d621b3fef82c2384759e5cb1618ffe360ea6af"
},
"acquisitions-final.html": {
"bytes": 14995,
"sha256": "966e5d89916f6e51ad916cbb42e5d363e6ca55d595bb6ac440a3517c879076a2"
},
"acquisitions-final.pdf": {
"bytes": 121359,
"sha256": "4b42456d5b2f52c48c7a08dda6c37c82bedfb653850864f902731845b28c34ba"
}
},
"order_vs_invoice_period": "order month preserved as origin, invoice/deduction month independent; directed test passed"
},
"limits": [
"projection and effective data are user-declared, not certification or AT submission",
"full taxable use and same invoice/deduction period only",
"outside-scope fields remain indeterminate unless explicit absence of operations",
"visual harness stores locally; real private server persistence proven separately with identical payload",
"IRPC/IRPS inventory only; no implementation"
],
"source_files": [
{
"path": "resources/js/lib/iva-acquisitions.ts",
"sha256": "c437ae4f218e6be1131fe05cd4bba0b7cc9c8d605803a418a9a035af32fb8432"
},
{
"path": "resources/js/components/simulation-acquisitions.tsx",
"sha256": "bb1c5dbecb2e00f1711397291b44b8bd78f8ae8f56c02c580f4fa68156a52013"
},
{
"path": "resources/js/lib/iva-modelo-a.ts",
"sha256": "c641619a05613c1620385baad0e9ffbf810dd970aafb30e12b3a7f9a3bf11846"
},
{
"path": "resources/js/components/simulation-monthly-iva.tsx",
"sha256": "db04651d99293b61dec9b94e9aaef28df5cb8d3cefc5e2cb3ebd2fe01ebd33d3"
},
{
"path": "resources/js/lib/project-dossier.ts",
"sha256": "22348747ce791f378dc42def531e6a0c22dea917f9ed0ccae4880fac104b0c09"
},
{
"path": "app/Http/Requests/StoreSimulationFormDraftRequest.php",
"sha256": "a75ed84f613a061775383836d9e0999a78b5ee150f548c9ee4313a795f4fff6b"
},
{
"path": "resources/js/components/simulation-modelo-a.tsx",
"sha256": "cf5b09b125fc7cd4c53105c2e67acf39d0e998a6c46c2f447a9f716445994e84"
}
],
"candidate_revision": "d18ff0f0499337579569fab2773f1769b5fd1ea6",
"branch": "codex/modelo-a-aquisicoes-20261024",
"integration_status": "not merged or deployed; QA candidate only"
},
"modelo_a_acquisitions_homologation_20261024": {
"status": "installed_delta_verified",
"authorization": "Tempo: integrate accepted QA candidate and deliver through existing Git process, preserving concurrent work and S1-S3; check only installed delta",
"functional_candidate": "d18ff0f0",
"QA_record": "1655f57e",
"served_commit": "1655f57e698fef4099c2886069a79cd1c920f792",
"previous_main": "cd10cccbf45f53836d8bf7b2bd90794d99602cb9",
"Git_integration": "fetch; fast-forward push to existing main, no force; webhook deployment",
"installed_delta": {
"status": "installed_delta_verified",
"served_revision": {
"commit": "1655f57e698fef4099c2886069a79cd1c920f792",
"branch": "main",
"built_at": "2026-10-24T10:18:11.360273+00:00",
"manifest_sha256": "dfb82481796aaf5826e8a5108575de3807ee787faffa92f38a56bb8d9caf5ce7"
},
"manifest_hash_matches": true,
"compiled_delta_markers": {
"modelo-a-internal-acquisitions/1": [
"assets/project-dossier-BsPlsWGS.js"
],
"sourcePeriod": [
"assets/project-dossier-BsPlsWGS.js",
"assets/services-DFQsTg9-.js"
],
"modelo_a_snapshot": [
"assets/project-dossier-BsPlsWGS.js",
"assets/services-DFQsTg9-.js"
],
"Reutilizar compras e períodos existentes": [
"assets/services-DFQsTg9-.js"
],
"Calcular e guardar versão do apuramento parcial": [
"assets/services-DFQsTg9-.js"
],
"Sem operações no campo": [
"assets/services-DFQsTg9-.js"
],
"sourceAmount": [
"assets/project-dossier-BsPlsWGS.js",
"assets/services-DFQsTg9-.js"
],
"same-origin": [
"assets/project-dossier-BsPlsWGS.js",
"assets/services-DFQsTg9-.js"
],
"recover:": [
"assets/services-DFQsTg9-.js"
]
},
"assets": [
{
"file": "assets/simulation-form-CPZAFZU3.js",
"status": 200,
"content_type": "application/javascript; charset=utf-8",
"sha256": "c6bcf64e60a5eeac0cafe3dc055dc5544162f21942ee08b34d51bc6d52947567"
},
{
"file": "assets/project-dossier-document-DpzdERdE.js",
"status": 200,
"content_type": "application/javascript; charset=utf-8",
"sha256": "2d2151a7287a4386092d280feea69bbc5078bcbf0db66527c1e641c1d56de6f0"
},
{
"file": "assets/project-dossier-BsPlsWGS.js",
"status": 200,
"content_type": "application/javascript; charset=utf-8",
"sha256": "e5f08542d48d70b01aad28c891f5f845ab80958e8233296d43f0b9c670cc0bf6"
},
{
"file": "assets/services-DFQsTg9-.js",
"status": 200,
"content_type": "application/javascript; charset=utf-8",
"sha256": "fb61aad61980d40299bbe73c0b7dcc9942b809e813ae63db80e16d07676d9d76"
}
],
"simulator_public_status": 200,
"simulator_uses_installed_app_entry": "assets/app-CPjUUqvb.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."
}
},
"qa_acceptance_tests_repeated": false,
"synthetic_dossiers_requests_or_calculations_created": 0,
"method": "read-only served revision, manifest, changed compiled features, simulator entry and existing availability; no active private write or database audit claimed",
"previous_homologation": "cd10cccb",
"candidate": "d18ff0f0",
"qa_record": "1655f57e",
"scope": "08-10 and conditional14-18 only; not complete IVA; IRPC/IRPS deferred",
"preservation": {
"S1_S3_original_sources_unchanged": true,
"accepted_S3_PDF_hash_unchanged": true,
"stabilization_reference_is_ancestor": true,
"schema_config_routes_published_rules_deploy_diff": [],
"concurrent_main_changes": "Fetched before integration: origin/main was cd10cccb and ancestor of candidate; no concurrent changes to reconcile; fast-forward without force",
"QA_proofs_reused": true
}
},
"balance": 0,
"scope": "acquisitions08-10 and conditional totals14-18 in demonstrated conditions; not complete IVA",
"IRPC_IRPS": "deferred, inventory preserved",
"statistics_AI": "deferred",
"protected_installer_or_database_audit": "not needed or performed; public served revision and delta verified; no synthetic private writes in homologation"
}
}
@@ -1,5 +1,81 @@
# Reconciliação do IVA com o Novo Modelo A
## 24/10/2026 — lote08–10/14–18 entregue na homologação em1655f57e
**Estado actual: integrado, instalado e delta conferido; saldo deste lote: zero.** Candidata funcional `d18ff0f0`, registo QA `1655f57e`; revisão efectivamente servida `1655f57e698fef4099c2886069a79cd1c920f792`. Fetch anterior à integração encontrou `origin/main` em `cd10cccb`, antepassado da candidata; push por fast-forward, sem force ou alterações concorrentes a reconciliar. O webhook do processo existente instalou a revisão. Não se editou a pasta servida.
Conferência exclusivamente do delta instalado: `/build/revision.json` em1655f57e, branchmain; manifesto SHA256 `dfb82481796aaf5826e8a5108575de3807ee787faffa92f38a56bb8d9caf5ce7`, igual ao anunciado pela revisão; quatro assets HTTP200, com nove marcadores da nova funcionalidade e recuperação existente. `/simulador-fiscal` HTTP200 usa a entrada deste build; disponibilidade existente do IVA geral mantida. Conteúdo dos bundles verificado contra o próprio manifesto instalado, sem presumir nomes/bytes iguais entre Windows e Linux.
Fontes de moeda/tesouraria, recuperação mensal e motor fiscal publicado, schema, configuração, rotas e executor sem delta face a `cd10cccb`. Referência `276d833a` antepassada; PDF aceite S3 conserva SHA256 `7719a05136f6dba668b621f551626060d10db5fcbb84815cdad112a1eab9079b`, sem regeneração. As provas QA abaixo foram reutilizadas. Sem importações, migrações ou pedidos/cálculos sintéticos criados na homologação. Não se afirma nova auditoria autenticada à base; a prova desta entrega é revisão, manifesto, assets e entradas servidos. Evidência privada: `outputs/iva-acquisitions-delivery-20261024/installed-delta.json`.
Fecho limitado às aquisições08–10 e totais14–18 nas condições demonstradas. Não é apuramento integral do IVA. IRPC/IRPS permanecem inventariados para trabalho posterior; estatísticas/IA adiadas. Os estados QA/pendência de integração seguintes são histórico anterior a esta disponibilização, não o saldo actual.
## 24/10/2026 — aquisições08–10 e totais14–18 implementados e demonstrados em QA
**Estado actual: lote concluído em QA, candidata `d18ff0f0`, ramo `codex/modelo-a-aquisicoes-20261024`; ainda não integrado/disponibilizado na homologação.** S1–S3 continuam encerrados em `cd10cccb`, recibo `355656b5`; referência `276d833a` preservada. Esta secção sucede à proposta e ao cotejo parcial históricos abaixo. Sem IRPC/IRPS, publicação de regras, migração, mudança de permissões ou escrita em dados activos.
### Cotejo concluído antes da codificação
`VAT Return.pdf`: 285479bytes, SHA256 `9c0b99a810c7db53eb75d63c22ee22fb86a50ba57bd5509041ed80ee10f75741`, duas páginas lidas e renderizadas. Comparação com a imagem dos dois DOCX sobre fundo branco e instruções já reconciliadas. Originais intactos. O PDF não fornece critérios adicionais de elegibilidade por aquisição, nem prova nova edição/vigência pela sua existência.
| Campo/instrução | PDF versus DOCX | Tratamento executável |
|---|---|---|
|08–10|Mesma numeração: imobilizado, existências, outros bens/serviços; coluna de imposto a favor do sujeito passivo|Somar apenas IVA elegível das linhas, não orçamento, custo ou valor bruto de compras|
|14|Mesma soma `01+03+05+06+07`|Determinar apenas com parcelas conhecidas|
|15|Mesma soma `08+09+10+11+12`|Aquisições08–10 completas e11/12 determinados; sem operações deve ser declaração expressa|
|16|Mesma soma `02+04+13`|Reutilizar02 da execução mensal actual;04/13 não são zero implícito|
|17/18|Mesmas diferenças `16−15` / `15−16`, conforme o sentido|Diferenças antes de créditos anteriores; não são pagamento ou reembolso reconhecido|
|Regularizações|Ambos dizem11/12 no texto, mas as caixas são12/13|Regularizações fora deste lote; divergência não bloqueia08–10 nem somas inequívocas|
|Créditos/reembolso|Mantêm-se as remissões18/19 versus19/20 e art.20 no rodapé|Afectam o futuro tratamento19–27; não são activadas por esta passagem|
|Preenchimentos/layout|PDF tem cabeçalho completo, duas páginas, inicial/dentro do prazo assinalados, UGC/Província/Localidade Maputo; DOCX tem caixas vazias e imagem do corpo, com fluxo de leitura diferente|Não herdar selecções, prazo ou Maputo para qualquer investimento; não corrigir ou substituir originais|
### Correspondência e percurso executáveis
Componente existente de IVA mensal conserva a execução01/02 no servidor. Novo bloco ligado ao mesmo formulário e relatório guarda `modelo_a_snapshot` no mecanismo privado/versionado existente, com livros separados pela oportunidade em tratamento. Sem escolha numa entrada com várias oportunidades, pede a escolha; nunca assume a primeira. A API conserva as permissões de proprietário e a concorrência por ETag.
Compras/encomendas são reutilizadas com valor/moeda/mês de origem, por classificar como líquido ou bruto. O mês de encomenda/orçamento é conservado separadamente do mês do documento e da dedução; adoptar uma origem actualizada não altera silenciosamente o período fiscal editado. Para08/10 é possível escolher explicitamente o orçamento de activos/despesas operacionais já existente como referência, sem o converter em factura. Só se pede a diferença fiscal/documental. Mudança da origem é sinalizada e exige adopção explícita; não substitui edições deliberadas. Moeda conhecida é conservada; moeda ausente exige confirmação expressa, sem conversão de montantes.
Por linha: líquido/IVA/bruto (dois valores permitem obter o terceiro por soma/diferença, sem taxa de compra inventada), documento/referência/linha/data, fornecedor/inscrição, posse/forma legal, período, natureza projectada ou efectiva declarada, aquisição interna, afectação e exclusões. O adquirente confirma regime normal de IVA. Documento repetido sem linha distinta é recusado. Uma compra só registada, orçamento, documento incompleto, uso misto/especial ou período divergente não gera dedução. IVA expressamente zero e despesas expressamente não elegíveis ficam distintos de informação em falta.
Fundamento dirigido: CIVA republicado pela Lei13/2016, PDF14–16, arts.18(1)(a),18(3–5),19(1)(a),20 e21(1–3); Lei22/2022, art.20 (exclusões/17-A/excepções); Lei10/2025, PDF13, arts.18(2)/(6) e19(2). Reutilização das fontes oficiais aceites, sem nova validação de credibilidade. Âmbito executado: interno geral, MZN, 2026, afectação exclusiva a operações tributadas, mesmo período de emissão/dedução, sem exclusões/especialidades/dedução parcial. É o recorte técnico solicitado, não prazo de vigência legal nem nova regra organizacional. Em projecções, as condições são pressupostos explícitos, sem certificação documental.
Por mês, confirmação expressa de lista completa08–10 e de inexistência de operações nos campos fora deste lote. Sem confirmação, as parcelas e totais dependentes permanecem indeterminados; declarações em conflito com importações/vendas de outros enquadramentos registadas são recusadas.14–18 usam aritmética decimal exacta. Resultados, condições e origem ficam congelados; alterações tornam a versão histórica visivelmente desactualizada. O relatório reutiliza o resultado guardado sem recalcular e identifica oportunidade, localização, período, moeda, fontes, versão e declarações de ausência. A nota da execução isolada01/02 remete agora para o bloco separado, evitando apresentar dois estados contraditórios.
### Provas dirigidas e saldo
- **11 testes frontend aprovados:** oito do novo lote e três da correspondência afectada. Condições incompletas, exclusões, uso misto, documento/fornecedor, data/moeda, duplicações, ausência versus zero, origem alterada, orçamentos reutilizados, fórmulas08–10/14–18, histórico e segunda oportunidade. Exemplo com08=32,09=16,10=8:15=56;16=160.0048;17=104.0048. Também diferença credora18 e vendas em falta.
- **API real QA: dois testes/19 asserções aprovados**, base isolada `simulator_fiscal_delta_20261021_test:55441`, guarda de identidade e transacções revertidas. Payload calculado na interface da segunda oportunidade exportado por acção própria de QA → gravação privada real → recuperação exacta → relatório. Outra conta recusada; primeira oportunidade preservada; fingerprint de todas as regras igual. Nenhuma regra criada/publicada/suspensa.
- **Visual Edge, componentes reais numa interface isolada:** compra sem condições fica indeterminada; edição/cálculo da segunda produz17=`128.0048`; actualizar a página conserva a segunda; regressar à primeira conserva17=`144.0048`. O armazenamento da interface visual é local ao browser; a persistência no servidor foi comprovada separadamente com o mesmo payload na API real de QA.
- **Relatório da segunda oportunidade recuperada pela API:** CSV, Word e PDF apresentam `128.0048`, `160.0048`, MZN, Manica, oportunidade310, período e versão correspondentes, sem valores do livro309. PDF final com seis páginas conferidas visualmente, sem cortes/sobreposição. Abril deliberadamente sem vendas mantém01/02/14/16/17/18 indeterminados; a projecção financeira incompleta é identificada, sem fabricar resultado. Nenhum PDF ou exportação aceite de S1–S3 foi substituído.
- **TypeScript e build aprovados.** Provas privadas em `outputs/iva-operations-qa-20261024`; relatório `acquisitions-final.pdf`, hashes no recibo integrado. A interface de ensaio não é incluída nos assets do portal.
Saldo deste recorte em QA: zero. Integração e disponibilização na homologação continuam por executar. Futuro fora deste recorte: activação de taxa reduzida/isenções, mistos/pro rata/excepções, importações, regularizações, créditos19–27, juros/pagamento/reembolso e submissão AT. IRPC/IRPS permanecem apenas inventariados. Estatísticas e IA adiadas. O histórico abaixo não reabre S1–S3 nem converte esta entrega QA em homologação.
## Actualização de 24/10/2026 — comparação e próximo lote proposto
S1–S3 permanecem encerrados na homologação em `cd10cccb`, recibo `355656b5`, saldo zero. Esta passagem é reconciliação documental/código e proposta; não altera regras, dados activos ou impostos. Reutiliza a leitura visual e as provas aceites de 21/10, sem repetir ensaios.
Os dois originais nas Notas foram relidos por bytes: tamanhos, SHA256 e imagem incorporada coincidem com os valores documentados abaixo. Inventário actual completo das duas pastas: nove documentos nas Notas e quatro em Modelos, além de seis ficheiros temporários/de bloqueio inventariados separadamente, registados com nome, tamanho e hash no recibo integrado, chave `modelo_a_next_scope_20261024`. Os quatro modelos são `VAT Return.pdf`, `IRPC_IRPS.xlsx`, `Cópia de IRPS-IMOP.xlsx` e `IRPC M-22- DECLARACAO DE RENDIMENTOS.pdf`. Os três de IRPC/IRPS entram apenas no inventário; conteúdo fiscal/fórmulas ainda não analisados. `VAT Return.pdf` é exemplar adicional inventariado, ainda por cotejar com os dois DOCX antes de afirmar equivalência ou diferença. Nenhum original alterado, importado ou publicado nesta passagem.
| Bloco | Já suportado / reutilizável | Falta concreta |
|---|---|---|
| Identificação e período, quadros 1–6 | Oportunidade escolhida, dossier, organização, contactos, meses, moeda e origem | Ligar os campos fiscais; confirmar apenas diferenças de NUIT/sucursal/área fiscal/CAE/domicílio fiscal. Sector e localização do projecto não provam CAE ou domicílio fiscal. Tipo de declaração não é versão de projecção. |
| Campos 01/02 | Vendas mensais líquidas e IVA calculado pela versão publicada, recuperação pelo ID, moeda original e exportações aceites | Sem lacuna neste âmbito aceite; continua componente de planeamento, não imposto final a pagar. |
| Campos 03–07 | Calendário de vendas como origem | Classificação por operação, bases separadas, âmbito temporal das categorias e regras correspondentes; mapeamento não é cálculo implementado. |
| Campos 08–11 | Compras/encomendas mensais e pressupostos do plano | Valores líquido/IVA/bruto discriminados, documentos, fornecedor, afectação, exclusões e elegibilidade. Compras e investimento global não são IVA dedutível. |
| Campos 12/13 | Correspondência documental | Regularizações individualizadas, motivo, documento e período; resolver a remissão interna divergente. |
| Campos 14–18 | Fórmulas impressas mapeadas | Consolidação: 14=01+03+05+06+07; 15=08+09+10+11+12; 16=02+04+13; diferenças devedora/credora 17/18. Actualmente indeterminadas: parcelas ausentes não são zero. |
| Campos 19–27 | Planeamento pode conservar estimativas separadas | Créditos efectivos/comunicados, substituição, juros, pagamento, reporte e pedido de reembolso. Não implementados como apuramento declarativo. |
| Quadros 7, 12 e 13 | Autenticação e histórico do portal | Inexistência de operações exige informação completa; assinatura/submissão fiscal e campos reservados à AT não decorrem do login ou da simulação. |
**Diferenças e instruções:** os DOCX diferem por bytes e segmentação textual, com a mesma imagem do formulário; não está demonstrada alteração fiscal entre eles. Mantêm-se as divergências já documentadas: regularizações 11/12 no texto versus caixas 12/13; créditos 18/19 versus caixas 19/20; remissão de reembolso art.20 versus alteração de 2025 no art.21; remissões ambíguas ao quadro8. A duplicação de25 era artefacto de extracção, já resolvido. A edição/período aplicável e os dispositivos pertinentes devem ser cotejados para cada regra nova, reutilizando as fontes oficiais aceites.
**Proposta de lote seguinte — IVA por operação, primeiro âmbito interno geral em MZN:** reutilizar vendas e aquisições do plano por oportunidade/período; guardar origem/versionamento, previsto ou efectivo e classificação explícita. Implementar a discriminação líquido/IVA/bruto e a elegibilidade documentada das aquisições internas de imobilizado, existências e outros bens/serviços (08–10), após cotejo dirigido das condições e exclusões nas fontes existentes. Manter 01/02 e o motor publicado. Ligar 14–18 apenas quando todas as parcelas necessárias estiverem conhecidas ou expressamente declaradas sem operações, sem permitir que essa declaração apague operações existentes. Mostrar a parcela em falta quando o total não puder ser determinado.
Ficam fora deste lote proposto: activação de taxa reduzida, isenções/mistas/pro rata, importações, regularizações, créditos anteriores, substituições, juros, pagamentos, reembolsos e submissão à AT. Podem ser identificados na classificação, mas mantêm resultado indeterminado e explicação quando aplicáveis. IRPC/IRPS permanecem inventário. A proposta estreita o lote histórico abaixo; a estimativa histórica de28–52h não é orçamento deste novo recorte.
Fecho dirigido do lote proposto: reutilização sem preenchimento repetido; aquisição elegível/excluída/sem documento; zero versus ausência; alteração da segunda oportunidade preservando a primeira; mudança de origem cria versão sem reescrever histórico; gravação/recuperação e ecrã/exportações com iguais montantes, moeda, período, oportunidade, fonte e regra. Consolidar apenas os dispositivos efectivamente usados, sem nova validação genérica das fontes. Nenhuma implementação iniciada nesta reconciliação.
21/10/2026. Referência AUD-SIM-INT02, sem nova matriz/backlog. Preparação progressiva em QA; não é apuramento completo, submissão ou aceitação pela AT. A componente16% existente continua independente das restantes componentes por cadastrar/desenvolver. A leitura e aceitação documental da Tempo não é apresentada como validação jurídica independente.
## Exemplar e preservação
@@ -1,5 +1,73 @@
# Rastreabilidade, utilização e formação
## 24/10/2026 — aquisições Modelo A entregues na homologação, revisão1655f57e
Lote08–10/14–18 integrado pelo Git existente e instalado em `1655f57e`, candidata `d18ff0f0`. Saldo deste lote zero. Revisão servida, manifesto, quatro assets/nove marcadores, entrada do simulador e disponibilidade existente conferidos apenas no delta. Sem novos pedidos/cálculos privados na homologação ou repetição dos ensaios aceites. S1–S3 `cd10cccb`/`355656b5`, PDF aceite e referência `276d833a` preservados; schema/configuração/regras publicadas sem alteração. Recibo integrado `modelo_a_acquisitions_homologation_20261024`; cotejo, âmbito e condições em `../simulations/modelo-a-reconciliacao-20261021.md`. Não é apuramento integral do IVA; IRPC/IRPS posteriores, estatísticas/IA adiadas. As secções de espera abaixo são histórico.
## 24/10/2026 — lote de aquisições Modelo A concluído em QA, candidata d18ff0f0
S1–S3 mantidos na homologação em `cd10cccb`/`355656b5`. Cotejo do `VAT Return.pdf` concluído:08–10 e14–18 correspondem aos DOCX; selecções/Maputo do PDF não são dados do investidor. Novo bloco no simulador: reutilizar compras/orçamentos explicitamente, completar documento/IVA/afectação/condições, confirmar cobertura por mês, calcular e guardar; a recuperação e o relatório usam a oportunidade escolhida. Ausência de condições/parcelas mantém resultado indeterminado. Mudanças conservam o resultado histórico e explicam a divergência. Regime e moeda não são inferidos da organização.
QA:11 testes frontend, dois testes de API/19 asserções, types/build e cadeia interface → API isolada → recuperação → relatório demonstrados. Segunda oportunidade17=128.0048, primeira144.0048 preservada. PDF novo de seis páginas revisto; provas S1–S3 reutilizadas. Detalhes e limites em `../simulations/modelo-a-reconciliacao-20261021.md`, recibo integrado `modelo_a_acquisitions_qa_20261024`, mesma linhaAUD-SIM-INT02. **Ainda não entregue na homologação.** IRPC/IRPS apenas inventário; estatísticas/IA adiadas.
## 24/10/2026 — próximo âmbito do Modelo A, apenas proposto
S1–S3 encerrados em `cd10cccb`, recibo `355656b5`. Reconciliação actualizada em `../simulations/modelo-a-reconciliacao-20261021.md`: campos01/02 suportados; restantes componentes discriminadas; proposta limitada a operações internas gerais, aquisições08–10 e totais condicionados14–18. Inventário completo das pastas Notas/Modelos no recibo integrado, incluindo IRPC/IRPS sem implementação. Sem novos impostos, regras activadas ou repetição de provas aceites. AUD-SIM-INT02 mantém o saldo de desenvolvimento do Modelo A distinto do lote S1–S3 encerrado.
## 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.
File diff suppressed because one or more lines are too long
@@ -0,0 +1,62 @@
import { useState } from 'react';
import { acquisitionBook, acquisitionCurrent, acquisitionExcludedFields, acquisitionPlanReferences, acquisitionReportRows, calculateAcquisitions, newAcquisition, reuseAcquisitions, saveAcquisitionBook, type Acquisition, type AcquisitionBook } from '@/lib/iva-acquisitions';
export function SimulationAcquisitions({draft,onChange}:{draft:Record<string,unknown>;onChange:(field:string,value:string)=>void}) {
const [error,setError]=useState('');
let book:AcquisitionBook;
try {book=acquisitionBook(draft);} catch(cause) {return <p role="alert">{String(cause)}</p>;}
const save=(next:AcquisitionBook)=>{try {onChange('modelo_a_snapshot',saveAcquisitionBook(draft,next));setError('');}catch(cause){setError(String(cause));}};
const change=(id:string,patch:Partial<Acquisition>)=>save({...book,rows:book.rows.map(row=>row.id===id?{...row,...patch}:row)});
const run=(action:()=>void)=>{try{action();setError('');}catch(cause){setError(cause instanceof Error?cause.message:String(cause));}};
let references:ReturnType<typeof acquisitionPlanReferences>=[];
try {references=acquisitionPlanReferences(draft);}catch { /* The action reports the concrete currency/source error without replacing saved values. */ }
const labels:Record<string,string>={'03':'Base à taxa reduzida','04':'IVA à taxa reduzida','05':'Isentas com direito à dedução','06':'Operações art.18(1)(b)','07':'Isentas sem direito','11':'Importações','12':'Regularizações a favor do sujeito passivo','13':'Regularizações a favor do Estado'};
const select=(row:Acquisition,key:keyof Acquisition,label:string,options:Array<[string,string]>)=><label className="block" key={key}>{label}<select className="block rounded border p-2" value={String(row[key]??'')} onChange={e=>change(row.id,{[key]:e.target.value})}><option value="">Por indicar</option>{options.map(([value,text])=><option key={value} value={value}>{text}</option>)}</select></label>;
const yesNo:Array<[string,string]>=[['yes','Sim'],['no','Não']];
return <section aria-label="Aquisições e apuramento parcial Modelo A" className="space-y-4 rounded border p-3">
<h4 className="font-semibold">Compras e apuramento parcial do Modelo A</h4>
<p>Oportunidade em tratamento: {String(draft.context_opportunity||'entrada geral')}. Moeda de origem: {book.currency||'por indicar'}. A compra registada não gera automaticamente IVA dedutível.</p>
{book.currency && draft.currency!==book.currency && <p role="alert">As compras conservam {book.currency}; o projecto está em {String(draft.currency||'moeda por indicar')}. Reveja a moeda no projecto. Os montantes e o resultado anterior não foram convertidos ou reinterpretados.</p>}
{!book.currency && !!draft.currency && <button type="button" className="underline" onClick={()=>save({...book,currency:String(draft.currency),rows:book.rows.map(row=>row.sourceCurrency?row:{...row,sourceCurrency:String(draft.currency)})})}>Confirmar explicitamente {String(draft.currency)} como moeda de origem dos valores sem moeda, conservando os montantes</button>}
<p>Primeiro âmbito: aquisições internas em MZN, afectação exclusiva a operações tributadas, sem exclusões ou dedução parcial, documento e dedução no mesmo período de 2026. Nas projecções, cada confirmação é um pressuposto, sem certificar documento ou declaração efectiva. Indique dois valores entre líquido, IVA e bruto; o terceiro é obtido por diferença ou soma, sem taxa de compra presumida.</p>
<label className="block">Adquirente sujeito passivo de IVA no regime normal (efectivo ou pressuposto da projecção)<select className="block rounded border p-2" value={book.taxpayerRegistered??''} onChange={e=>save({...book,taxpayerRegistered:e.target.value as AcquisitionBook['taxpayerRegistered']})}><option value="">Por confirmar</option><option value="yes">Sim</option><option value="no">Não / outro enquadramento — fora deste lote</option></select></label>
{error&&<p role="alert">{error}</p>}
<button type="button" className="underline" onClick={()=>run(()=>save(reuseAcquisitions(draft,book)))}>Reutilizar compras e períodos existentes</button>
<button type="button" className="ml-3 underline" disabled={!book.periods.length} onClick={()=>save({...book,rows:[...book.rows,newAcquisition(crypto.randomUUID(),book.periods[0]?.period??'',book.currency)]})}>Acrescentar aquisição em falta</button>
{book.rows.map(row=><details key={row.id} className="rounded border p-3"><summary>{row.description||'Aquisição adicional'} — {row.period} — campo{row.field}</summary>
<p>Origem: {row.sourceMonth===null?'Introdução directa':`Plano, mês ${row.sourceMonth} (${row.sourcePeriod??'por indicar'}): ${row.sourceAmount??'sem informação'} ${row.sourceCurrency}`}. O mês da encomenda/orçamento não determina a data do documento ou a dedução. Alterações não substituem o resultado anterior.</p>
<div className="grid gap-3 md:grid-cols-2">
<label>Descrição<input className="block rounded border p-2" value={row.description} onChange={e=>change(row.id,{description:e.target.value})}/></label>
{row.sourceMonth===null && <label>Reutilizar valor existente, sem presumir que o orçamento é uma factura<select className="block rounded border p-2" value="" onChange={e=>run(()=>{const ref=references.find(r=>r.key===e.target.value);if(ref)change(row.id,{sourceMonth:ref.month,sourcePeriod:ref.period,sourceField:ref.kind,sourceAmount:ref.amount,sourceCurrency:ref.currency,...(ref.kind==='asset'?{}:{period:ref.period})});})}><option value="">Escolher apenas se corresponde a esta aquisição</option>{references.filter(ref=>ref.kind===(row.field==='08'?'asset':row.field==='10'?'operating':'inventory')).map(ref=><option key={ref.key} value={ref.key}>{ref.label}: {ref.amount??'sem informação'} {ref.currency}</option>)}</select></label>}
{select(row,'field','Categoria',[['08','08 — Imobilizado'],['09','09 — Existências'],['10','10 — Outros bens e serviços']])}
{select(row,'origin','Natureza dos dados',[['projected','Projectados / pressupostos'],['effective','Efectivos declarados']])}
<label>Período de dedução<input className="block rounded border p-2" type="month" value={row.period} onChange={e=>change(row.id,{period:e.target.value})}/></label>
{row.sourceMonth!==null && <label>O valor de origem corresponde a<select className="block rounded border p-2" value={row.sourceBasis} onChange={e=>{const basis=e.target.value as Acquisition['sourceBasis'];change(row.id,{sourceBasis:basis,...(basis==='net'&&!row.net?{net:row.sourceAmount??''}:basis==='gross'&&!row.gross?{gross:row.sourceAmount??''}:{})});}}><option value="">Por classificar</option><option value="net">Valor sem IVA</option><option value="gross">Valor com IVA</option></select></label>}
{(['net','vat','gross'] as const).map((field,index)=><label key={field}>{['Valor líquido','IVA indicado','Valor bruto'][index]} ({row.sourceCurrency})<input className="block rounded border p-2" inputMode="decimal" value={row[field]} onChange={e=>change(row.id,{[field]:e.target.value})}/></label>)}
{select(row,'documentType','Tipo de documento',[['invoice','Factura'],['equivalent','Documento equivalente']])}
<label>Referência do documento / previsão<input className="block rounded border p-2" value={row.documentRef} onChange={e=>change(row.id,{documentRef:e.target.value})}/></label>
<label>Linha do documento, se discriminado em várias aquisições<input className="block rounded border p-2" value={row.documentLine} onChange={e=>change(row.id,{documentLine:e.target.value})}/></label>
<label>Data do documento / prevista<input className="block rounded border p-2" type="date" value={row.documentDate} onChange={e=>change(row.id,{documentDate:e.target.value})}/></label>
<label>Fornecedor / previsto<input className="block rounded border p-2" value={row.supplier} onChange={e=>change(row.id,{supplier:e.target.value})}/></label>
{select(row,'domestic','Aquisição interna',yesNo)}
{select(row,'supplierRegistered','Fornecedor inscrito no IVA',yesNo)}
{select(row,'legalDocument','Documento emitido em forma legal',yesNo)}
{select(row,'held','Documento na posse do adquirente / previsto na projecção',yesNo)}
{select(row,'allocation','Afectação',[['taxable_only','Exclusiva a operações tributadas'],['mixed','Mista / parcial — fora deste lote'],['no_right','Operações sem direito à dedução']])}
{select(row,'exclusion','Exclusões de dedução',[['none','Sem exclusões ou casos especiais dos arts.18–20'],['excluded','Despesa excluída'],['special','Veículos, combustível, viagens, alojamento, comunicações, luxo ou outra situação especial — por enquadrar']])}
{select(row,'integrity','Operação real / pressuposto regular, sem simulação, margem de segunda mão ou impedimentos do art.18(3–5)',yesNo)}
</div>
<p>Fundamento: CIVA arts.18–21, fontes e versão no resultado. Não se calcula dedução parcial, excepção de despesa ou dedução posterior neste lote.</p>
{row.sourceMonth===null && <button type="button" className="mr-3 underline" onClick={()=>save({...book,rows:book.rows.filter(r=>r.id!==row.id)})}>Remover esta aquisição introduzida directamente</button>}
{row.sourceMonth!==null && <button type="button" className="underline" onClick={()=>run(()=>{const ref=acquisitionPlanReferences(draft).find(r=>r.month===row.sourceMonth && r.kind===(row.sourceField??'inventory'));if(!ref)throw new Error('Origem não disponível; conserve o registo e reveja o plano.');if(draft.currency!==row.sourceCurrency)throw new Error('Moeda divergente; não adoptar montantes de outra moeda.');change(row.id,{sourceAmount:ref.amount,sourcePeriod:ref.period,...(row.sourceBasis==='net'?{net:ref.amount??'',gross:''}:row.sourceBasis==='gross'?{gross:ref.amount??'',net:''}:{})});})}>Adoptar explicitamente o valor actual do plano (conserva histórico)</button>}
</details>)}
{book.periods.map(period=><details key={period.period} className="rounded border p-3"><summary>{period.period} — cobertura das parcelas — {period.complete && period.absent.length===acquisitionExcludedFields.length?'confirmada':'por completar'}</summary>
<label className="block"><input type="checkbox" checked={period.complete} onChange={e=>save({...book,periods:book.periods.map(p=>p.period===period.period?{...p,complete:e.target.checked}:p)})}/> Incluí todas as aquisições08–10 deste período, incluindo as sem direito à dedução; uma lista vazia significa que não houve essas aquisições.</label>
<p>Campos fora deste lote: assinale apenas quando não existiram as respectivas operações. Uma parcela não assinalada permanece indeterminada.</p>
{acquisitionExcludedFields.map(field=><label className="block" key={field}><input type="checkbox" checked={period.absent.includes(field)} onChange={e=>save({...book,periods:book.periods.map(p=>p.period===period.period?{...p,absent:e.target.checked?[...p.absent,field]:p.absent.filter(f=>f!==field)}:p)})}/> Sem operações no campo{field} — {labels[field]}</label>)}
</details>)}
<button type="button" disabled={!book.periods.length} className="rounded bg-emerald-800 p-2 text-white disabled:opacity-50" onClick={()=>run(()=>save({...book,result:calculateAcquisitions(draft,book)}))}>Calcular e guardar versão do apuramento parcial</button>
<p>O preenchimento e o resultado seguem a gravação privada do simulador. Os campos19–27 e a submissão à AT permanecem fora deste lote; 17/18 são diferenças antes de créditos anteriores, sem ordem de pagamento ou promessa de reembolso.</p>
{book.result&&<><p role="status">{acquisitionCurrent(draft,book)?'Resultado correspondente às entradas guardadas.':'Entradas ou origem alteradas: resultado histórico preservado; actualizar o cálculo.'}</p><dl>{acquisitionReportRows(draft).map(([,label,value,origin],i)=><div key={i}><dt className="font-semibold">{label}</dt><dd>{value}<small className="block">{origin}</small></dd></div>)}</dl></>}
</section>;
}
@@ -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,
}),
],
@@ -8,7 +8,7 @@ export function SimulationModeloA({draft}: {draft:Record<string,unknown>}) {
return <section aria-label="Correspondência com o Modelo A" className="space-y-3 rounded border p-3">
<h4 className="font-semibold">Modelo A — correspondência da simulação</h4>
<p>{modeloAReference.status}</p>
<p>Os campos 14–18 do apuramento permanecem indeterminados. O IVA das vendas não é imposto final a pagar. Os dados deste percurso são projecções, separados de facturas e declarações efectivas.</p>
<p>Esta correspondência descreve a componente de vendas01/02. Consulte o bloco de compras e apuramento parcial para os campos08–10 e14–18, que só são determinados com as parcelas necessárias completas. O IVA das vendas não é imposto final a pagar.</p>
<details><summary>Aquisições já projectadas — sem dedução presumida</summary>
<p>Reutilizam-se os valores das compras/encomendas para existências do plano. O mês da encomenda não prova a data da factura, recepção ou pagamento. Valores declarados ainda sem indicação de base líquida/IVA incluído não são tratados como base tributável. IVA suportado e IVA dedutível continuam por determinar; zero de compras não prova inexistência de outras aquisições.</p>
{error && <p role="alert">{error}</p>}
@@ -1,9 +1,10 @@
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';
import { SimulationAcquisitions } from './simulation-acquisitions';
export function SimulationMonthlyIva({draft, busy, onCalculate, onChange}: {
draft: Record<string, unknown>; busy: boolean;
@@ -16,6 +17,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 +27,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 +38,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,11 +78,13 @@ 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>}
{snapshot && <details><summary>Ver fundamento e relatório da versão guardada</summary><dl>{fiscalQaRows(snapshot).map(([label,value], index) => <div key={index}><dt className="font-semibold">{label}</dt><dd>{value}</dd></div>)}</dl></details>}
<SimulationAcquisitions draft={draft} onChange={onChange} />
<SimulationModeloA draft={draft} />
</section>;
}
+182
View File
@@ -0,0 +1,182 @@
import { modeloAPurchaseReferences } from './iva-modelo-a.ts';
import { monthlyIvaInput, monthlyIvaKey, readMonthlyIvaRecord, readMonthlyIvaSettings } from './monthly-iva.ts';
import { restoreBusinessPlanSnapshot } from './business-financial-plan.ts';
export const acquisitionVersion = 'modelo-a-internal-acquisitions/1';
export const acquisitionBasis = 'CIVA arts.18(1)(a),18(2) (Lei10/2025),18(3–5),19(1)(a),19(2),20 (Lei22/2022),21(1–3); âmbito interno, afectação exclusiva, mesmo período, sem exclusões ou dedução parcial.';
export const acquisitionSources = 'Modelo A DOCX 62c198e9 / v1 63d45fa8; VAT Return.pdf SHA256 9c0b99a810c7db53eb75d63c22ee22fb86a50ba57bd5509041ed80ee10f75741, p.1; CIVA republicado Lei13/2016, PDF14–16; Lei22/2022, art.20; Lei10/2025, arts.18/19, PDF13.';
type Choice = '' | 'yes' | 'no';
export type Acquisition = {
id: string; field: '08'|'09'|'10'; period: string; origin: 'projected'|'effective';
sourceMonth: number|null; sourcePeriod?:string; sourceField?:'inventory'|'asset'|'operating'; sourceAmount: string|null; sourceCurrency: string; sourceBasis: ''|'net'|'gross';
net: string; vat: string; gross: string; documentType: ''|'invoice'|'equivalent'; documentRef: string; documentDate: string; documentLine:string;
supplier: string; supplierRegistered: Choice; legalDocument: Choice; held: Choice; domestic: Choice;
allocation: ''|'taxable_only'|'mixed'|'no_right'; exclusion: ''|'none'|'excluded'|'special';
integrity: Choice; description: string;
};
export type AcquisitionResult = {id:string; net:string|null; vat:string|null; gross:string|null; deductible:string|null; reason:string};
type PeriodInput = {period:string; complete:boolean; absent:string[]};
export type AcquisitionBook = {currency:string; taxpayerRegistered?:Choice; rows:Acquisition[]; periods:PeriodInput[]; result?:AcquisitionCalculation};
export type AcquisitionCalculation = {version:string; revision:number; calculatedAt:string; opportunity:string; location:string; currency:string; sourceKey:string; basis:string; sources:string;
declarations?:{taxpayerRegistered:Choice;periods:PeriodInput[]}; rows:AcquisitionResult[]; inputs:Acquisition[]; periods:Array<{period:string; fields:Record<string,string|null>; reasons:string[]}>};
type Store = {version:1; books:Record<string,AcquisitionBook>};
const excluded = ['03','04','05','06','07','11','12','13'];
export const acquisitionExcludedFields = excluded;
export function acquisitionScope(draft:Record<string,unknown>):string {
if(!draft.context_opportunity) {
try {const context=JSON.parse(String(draft.context_applicability_snapshot??''));if(context.opportunity_choices?.length>1)throw new Error('Escolha a oportunidade em tratamento antes de preparar as compras.');}
catch(cause) {if(cause instanceof Error && cause.message.startsWith('Escolha'))throw cause;}
}
return String(draft.context_opportunity || 'general');
}
export function readAcquisitionStore(raw:unknown):Store {
if (!raw) return {version:1,books:{}};
const value=JSON.parse(String(raw));
if (value?.version!==1 || !value.books || typeof value.books!=='object' || Array.isArray(value.books)) throw new Error('Compras fiscais guardadas não recuperáveis; dados preservados.');
for(const book of Object.values(value.books) as AcquisitionBook[]) {
if(!book || !Array.isArray(book.rows) || !Array.isArray(book.periods) || typeof book.currency!=='string') throw new Error('Formato de compras não recuperável; sem substituição.');
const stringKeys=Object.keys(newAcquisition('','','')).filter(key=>!['sourceMonth','sourceAmount'].includes(key));
if(book.rows.some(row=>!row || stringKeys.some(key=>typeof row[key as keyof Acquisition]!=='string') || !(row.sourceMonth===null || Number.isInteger(row.sourceMonth)) || !(row.sourceAmount===null || typeof row.sourceAmount==='string')) || book.periods.some(p=>!p || typeof p.period!=='string' || typeof p.complete!=='boolean' || !Array.isArray(p.absent) || p.absent.some(f=>typeof f!=='string'))) throw new Error('Linhas ou períodos de compras não recuperáveis; os dados guardados foram preservados.');
}
return value;
}
export function acquisitionBook(draft:Record<string,unknown>):AcquisitionBook {
return readAcquisitionStore(draft.modelo_a_snapshot).books[acquisitionScope(draft)] ?? {currency:String(draft.currency ?? ''),rows:[],periods:[]};
}
export function saveAcquisitionBook(draft:Record<string,unknown>,book:AcquisitionBook):string {
const store=readAcquisitionStore(draft.modelo_a_snapshot);
return JSON.stringify({...store,books:{...store.books,[acquisitionScope(draft)]:book}});
}
export function newAcquisition(id:string,period:string,currency:string):Acquisition {
return {id,field:'09',period,origin:'projected',sourceMonth:null,sourceAmount:null,sourceCurrency:currency,sourceBasis:'',net:'',vat:'',gross:'',documentType:'',documentRef:'',documentDate:'',documentLine:'',supplier:'',supplierRegistered:'',legalDocument:'',held:'',domestic:'',allocation:'',exclusion:'',integrity:'',description:''};
}
function periods(draft:Record<string,unknown>):string[] {
try {return monthlyIvaInput(draft,readMonthlyIvaSettings(String(draft.monthly_iva_settings ?? ''))).monthly_sales.map(r=>r.date.slice(0,7));}
catch {return [];}
}
export function acquisitionPlanReferences(draft:Record<string,unknown>):Array<{key:string;kind:'inventory'|'asset'|'operating';month:number;period:string;amount:string|null;currency:string;label:string}> {
const dates=periods(draft),plan=restoreBusinessPlanSnapshot(String(draft.business_plan_snapshot??''));
if(!plan || !dates.length)return [];
if(plan.currency!==draft.currency)throw new Error('Moeda do plano divergente; não reutilizar como moeda do projecto.');
const inventory=modeloAPurchaseReferences(draft).map(r=>({key:`inventory:${r.month}`,kind:'inventory' as const,month:r.month,period:dates[r.month-1],amount:r.declared,currency:plan.currency,label:`Compras/encomendas — mês ${r.month}`}));
const asset={key:'asset:1',kind:'asset' as const,month:1,period:dates[0],amount:plan.assetInvestment?.trim()||null,currency:plan.currency,label:'Orçamento de investimento em activos — período a confirmar'};
const operating=dates.map((period,index)=>({key:`operating:${index+1}`,kind:'operating' as const,month:index+1,period,amount:(plan.monthlyOperatingExpensesByPeriod?plan.monthlyOperatingExpensesByPeriod[index]:plan.monthlyOperatingExpenses)?.trim()||null,currency:plan.currency,label:`Orçamento de despesas operacionais — mês ${index+1}`}));
return [...inventory,asset,...operating];
}
export function reuseAcquisitions(draft:Record<string,unknown>,book:AcquisitionBook):AcquisitionBook {
const dates=periods(draft); if(!dates.length) throw new Error('Defina o período mensal e resolva a moeda da projecção antes de reutilizar compras.');
const rows=[...book.rows];
for(const reference of modeloAPurchaseReferences(draft)) {
if(rows.some(r=>r.sourceMonth===reference.month)) continue;
rows.push({...newAcquisition(`plan-${reference.month}`,dates[reference.month-1],String(draft.currency ?? '')),sourceMonth:reference.month,sourcePeriod:dates[reference.month-1],sourceAmount:reference.declared,description:`Compras/encomendas do plano — mês ${reference.month}`});
}
return {...book,rows,periods:dates.map(period=>book.periods.find(p=>p.period===period) ?? {period,complete:false,absent:[]})};
}
// Decimal arithmetic for aggregation only. No binary rounding and no invented purchase tax rate.
function amount(raw:unknown):bigint|null {
if(typeof raw!=='string' || !/^\d{1,15}(\.\d{1,6})?$/.test(raw.trim())) return null;
const [a,b='']=raw.trim().split('.');return BigInt(a)*1000000n+BigInt(b.padEnd(6,'0'));
}
function money(value:bigint):string {const a=value/1000000n,b=(value%1000000n).toString().padStart(6,'0').replace(/0+$/,'').padEnd(2,'0');return `${a}.${b}`;}
function sum(values:Array<string|null>):string|null {const parsed=values.map(amount);return parsed.some(v=>v===null)?null:money((parsed as bigint[]).reduce((a,b)=>a+b,0n));}
function dateValid(raw:string):boolean {const date=new Date(`${raw}T00:00:00Z`);return /^\d{4}-\d{2}-\d{2}$/.test(raw) && Number.isFinite(date.getTime()) && date.toISOString().slice(0,10)===raw;}
export function evaluateAcquisition(row:Acquisition,currency:string,taxpayerRegistered:Choice=''):AcquisitionResult {
let n=amount(row.net),v=amount(row.vat),g=amount(row.gross);
if(n!==null && v!==null && g===null && !row.gross.trim()) g=n+v;
if(g!==null && v!==null && n===null && !row.net.trim() && g>=v) n=g-v;
if(n!==null && g!==null && v===null && !row.vat.trim() && g>=n) v=g-n;
const result:AcquisitionResult={id:row.id,net:n===null?null:money(n),vat:v===null?null:money(v),gross:g===null?null:money(g),deductible:null,reason:''};
const stop=(reason:string,deductible:string|null=null)=>({...result,reason,deductible});
if(currency!=='MZN' || row.sourceCurrency!==currency) return stop('Divergência ou moeda fora de MZN; sem conversão.');
if(!/^2026-(0[1-9]|1[0-2])$/.test(row.period)) return stop('Período fora do âmbito 2026 ou em falta.');
if(n===null || v===null || g===null || n+v!==g) return stop('Indique dois valores válidos; líquido + IVA deve corresponder ao bruto.');
if(row.sourceMonth!==null && (!row.sourceBasis || amount(row.sourceAmount)!==(row.sourceBasis==='net'?n:g))) return stop('Classifique ou resolva a divergência do valor de origem; não foi reinterpretado.');
if(v===0n) return stop('IVA explicitamente indicado como zero; não existe imposto a deduzir nesta aquisição.','0.00');
if(taxpayerRegistered!=='yes') return stop('Confirme o enquadramento do adquirente como sujeito passivo de IVA no regime normal; não é inferido da organização.');
if(row.allocation==='no_right' || row.exclusion==='excluded' || row.supplierRegistered==='no' || row.legalDocument==='no' || row.integrity==='no') return stop('Sem dedução no âmbito declarado: afectação, exclusão, fornecedor ou documento não elegível.','0.00');
if(row.domestic!=='yes') return stop('Aquisição interna por confirmar; importações fora deste lote.');
if(row.allocation!=='taxable_only' || row.exclusion!=='none') return stop('Confirme afectação exclusiva a operações tributadas e ausência de exclusões; casos mistos/especiais não calculados.');
if(!['invoice','equivalent'].includes(row.documentType) || !row.documentRef.trim() || !row.supplier.trim() || row.supplierRegistered!=='yes' || row.legalDocument!=='yes' || row.held!=='yes' || row.integrity!=='yes') return stop('Documento, fornecedor inscrito, posse e condições de dedução ainda incompletos.');
if(!dateValid(row.documentDate) || row.documentDate.slice(0,7)!==row.period) return stop('Data do documento e período de dedução devem coincidir neste lote; dedução posterior não presumida.');
return stop(row.origin==='projected'?'Dedução projectada sob pressupostos explícitos; não certifica factura ou declaração.':'Dedução segundo dados e condições declarados; sem submissão ou certificação pela AT.',money(v));
}
function sourceKey(draft:Record<string,unknown>,book:AcquisitionBook):string {
const raw=JSON.stringify([acquisitionScope(draft),draft.currency??'',draft.province_id??'',draft.district_id??'',draft.business_plan_snapshot,draft.monthly_iva_settings,draft.monthly_iva_snapshot,book.currency,book.taxpayerRegistered,book.rows,book.periods]);
// Change detection only, never an authorization or authenticity token.
let hash=14695981039346656037n;for(let i=0;i<raw.length;i++)hash=BigInt.asUintN(64,(hash^BigInt(raw.charCodeAt(i)))*1099511628211n);
return `${raw.length}:${hash.toString(16)}`;
}
function location(draft:Record<string,unknown>):string {
const labels=draft.selection_labels as Record<string,unknown>|undefined;
return ['province_id','district_id'].filter(key=>draft[key]).map(key=>String(draft[`${key}_label`]??labels?.[key]??draft[key])).join(' / ');
}
export function acquisitionCurrent(draft:Record<string,unknown>,book:AcquisitionBook):boolean {return !!book.result && book.result.version===acquisitionVersion && book.result.opportunity===acquisitionScope(draft) && book.result.sourceKey===sourceKey(draft,book);}
export function calculateAcquisitions(draft:Record<string,unknown>,book:AcquisitionBook,now=new Date().toISOString()):AcquisitionCalculation {
const scope=acquisitionScope(draft),dates=periods(draft);
if(book.currency!=='MZN' || draft.currency!==book.currency) throw new Error('Resolva a moeda de origem das compras; valores preservados sem conversão.');
if(!dates.length) throw new Error('Período de vendas por determinar.');
if(new Set(book.rows.map(r=>r.id)).size!==book.rows.length) throw new Error('Existem compras com identidade repetida.');
const documents=book.rows.filter(r=>r.documentRef.trim() && r.supplier.trim()).map(r=>JSON.stringify([r.origin,r.supplier.trim().toLowerCase(),r.documentRef.trim().toLowerCase(),r.documentDate,r.documentLine?.trim()??'']));
if(new Set(documents).size!==documents.length) throw new Error('Documento/linha repetido: discrimine linhas distintas ou remova a duplicação antes de deduzir.');
if(book.rows.some(r=>!['08','09','10'].includes(r.field) || !dates.includes(r.period))) throw new Error('Classifique cada aquisição num campo08–10 e num período da projecção.');
const references=acquisitionPlanReferences(draft);
for(const row of book.rows.filter(r=>r.sourceMonth!==null)) {
const current=references.find(p=>p.month===row.sourceMonth && p.kind===(row.sourceField??'inventory'));
if(!current || current.amount!==row.sourceAmount || (current.kind!=='asset' && row.sourcePeriod && current.period!==row.sourcePeriod)) throw new Error('As compras ou orçamentos do plano mudaram. Resolva a divergência de origem antes de calcular; histórico preservado.');
}
const rows=book.rows.map(row=>evaluateAcquisition(row,book.currency,book.taxpayerRegistered));
const record=readMonthlyIvaRecord(String(draft.monthly_iva_snapshot ?? ''));
let salesCurrent=false;
try {salesCurrent=!!record && record.execution.status==='completed' && monthlyIvaKey(record.input)===monthlyIvaKey(monthlyIvaInput(draft,readMonthlyIvaSettings(String(draft.monthly_iva_settings ?? ''))));} catch { /* Preserve history, never fabricate 01/02. */ }
const projection=record?.execution.result_snapshot?.monthly_projection as {rows?:Array<{date:string;net_sales:string|null;vat:string|null}>}|undefined;
const calculated=dates.map(period=>{
const input=book.periods.find(p=>p.period===period), reasons:string[]=[], fields:Record<string,string|null>={};
const declaredAmount=(key:string)=>{const value=amount(String(draft[key]??''));return value!==null && value>0n;};
if(input?.absent.includes('11') && (declaredAmount('imports') || book.rows.some(r=>r.period===period && r.domestic==='no'))) throw new Error('Existem importações declaradas. Não confirme ausência no campo11; enquadramento fora deste lote.');
if(input?.absent.includes('07') && declaredAmount('sales_exempt')) throw new Error('Existem vendas isentas declaradas. Resolva o período e a classificação antes de confirmar ausência no campo07.');
if(input?.absent.includes('05') && (declaredAmount('exports')||declaredAmount('sales_zero_rate'))) throw new Error('Existem exportações ou vendas de outro enquadramento. Não confirme ausência no campo05 sem resolver a classificação.');
const sales=salesCurrent?projection?.rows?.find(r=>r.date.slice(0,7)===period):null;
fields['01']=sales?.net_sales??null;fields['02']=sales?.vat??null;
if(!sales || sales.net_sales===null || sales.vat===null) reasons.push('01/02: vendas ou IVA mensal em falta, execução incompleta ou desactualizada.');
for(const field of excluded) fields[field]=input?.absent.includes(field)?'0.00':null;
for(const field of ['08','09','10']) {
const matching=book.rows.filter(r=>r.period===period && r.field===field);
fields[field]=input?.complete ? sum(matching.map(r=>rows.find(x=>x.id===r.id)!.deductible)) : null;
}
if(!input?.complete) reasons.push('Aquisições08–10: confirme que todas as compras do período foram incluídas, mesmo quando não existam.');
for(const row of book.rows.filter(r=>r.period===period)) {const r=rows.find(x=>x.id===row.id)!;if(r.deductible===null) reasons.push(`${row.description||row.id}: ${r.reason}`);}
for(const field of excluded) if(fields[field]===null) reasons.push(`Campo${field}: fora deste lote, sem declaração expressa de inexistência de operações.`);
fields['14']=sum(['01','03','05','06','07'].map(f=>fields[f]));
fields['15']=sum(['08','09','10','11','12'].map(f=>fields[f]));
fields['16']=sum(['02','04','13'].map(f=>fields[f]));
const state=amount(fields['16']),taxpayer=amount(fields['15']);
fields['17']=state===null||taxpayer===null?null:money(state>taxpayer?state-taxpayer:0n);
fields['18']=state===null||taxpayer===null?null:money(taxpayer>state?taxpayer-state:0n);
return {period,fields,reasons};
});
return {version:acquisitionVersion,revision:(book.result?.revision??0)+1,calculatedAt:now,opportunity:scope,location:location(draft),currency:book.currency,sourceKey:sourceKey(draft,book),basis:acquisitionBasis,sources:acquisitionSources,declarations:{taxpayerRegistered:book.taxpayerRegistered??'',periods:structuredClone(book.periods)},rows,inputs:structuredClone(book.rows),periods:calculated};
}
export function acquisitionReportRows(draft:Record<string,unknown>):Array<[string,string,string,string]> {
if(!draft.modelo_a_snapshot) return [];
try {
const book=acquisitionBook(draft),result=book.result;
if(!result) return [['IVA mensal','Compras — Modelo A','Sem cálculo guardado para a oportunidade escolhida','Sem dedução presumida']];
const context=`${result.currency}; oportunidade ${result.opportunity}; localização ${result.location || 'não indicada'}; ${result.version}; revisão ${result.revision}; ${result.calculatedAt}`;
const out:Array<[string,string,string,string]>=[['IVA mensal','Apuramento parcial Modelo A',acquisitionCurrent(draft,book)?'Versão correspondente às entradas':'Histórico desactualizado; não aplicar à projecção actual',context],['IVA mensal','Fundamento das aquisições',result.basis,result.sources]];
for(const row of result.inputs) {
const r=result.rows.find(x=>x.id===row.id)!;
const source=row.sourceMonth===null?'Introduzido pelo utilizador':`Plano (${row.sourceField==='asset'?'orçamento de activos':row.sourceField==='operating'?'orçamento operacional':'compras/encomendas'}), mês ${row.sourceMonth}, ${row.sourcePeriod??'período de origem não indicado'}, ${row.sourceAmount??'valor em falta'} ${row.sourceCurrency}; ${row.sourceBasis==='net'?'sem IVA':row.sourceBasis==='gross'?'com IVA':'base por classificar'}`;
const allocation={taxable_only:'exclusiva a operações tributadas',mixed:'mista',no_right:'sem direito à dedução','':'por indicar'}[row.allocation];
out.push(['IVA mensal',`Aquisição ${row.id} — campo${row.field} — ${row.period}`,`${row.description}; líquido ${r.net??'Indeterminado'}; IVA ${r.vat??'Indeterminado'}; bruto ${r.gross??'Indeterminado'}; dedutível ${r.deductible??'Indeterminado'}`,`${context}; ${row.origin==='projected'?'Projecção':'Dados efectivos declarados'}; ${source}; ${row.documentType==='invoice'?'Factura':row.documentType==='equivalent'?'Documento equivalente':'Documento por indicar'} ${row.documentRef}, ${row.documentDate}, linha ${row.documentLine||'única'}; fornecedor ${row.supplier}; afectação ${allocation}; ${r.reason}`]);
}
for(const period of result.periods) {
const entries=Object.entries(period.fields).sort(([a],[b])=>Number(a)-Number(b));
const declaration=result.declarations?.periods.find(p=>p.period===period.period);
const declarationOrigin=declaration?`Aquisições08–10 ${declaration.complete?'declaradas completas':'por completar'}; ausência de operações expressamente declarada nos campos ${declaration.absent.join(', ')||'nenhum'}.`:'Declarações detalhadas não recuperáveis nesta versão histórica.';
out.push(['IVA mensal',`Modelo A — ${period.period} — parcelas01–13`,entries.filter(([field])=>Number(field)<=13).map(([field,value])=>`${field}: ${value??'Indeterminado'}`).join('; '),`${context}; ${declarationOrigin}`]);
out.push(['IVA mensal',`Modelo A — ${period.period} — totais14–18`,entries.filter(([field])=>Number(field)>=14).map(([field,value])=>`${field}: ${value??'Indeterminado'}`).join('; '),context]);
if(period.reasons.length)out.push(['IVA mensal',`Modelo A — ${period.period} — pendências`,period.reasons.join(' '),result.sources]);
}
return out;
} catch {return [['IVA mensal','Compras — Modelo A','Não foi possível recuperar os dados guardados','Preservados, sem substituição por zero']];}
}
+1 -1
View File
@@ -45,7 +45,7 @@ export function modeloAComponentRows(snapshot: Record<string, unknown>): Array<[
return [
['Correspondência Modelo A',modeloAReference.status],
['Campos01/02', 'Base líquida e IVA mensal da versão publicada. Os restantes campos não foram presumidos iguais a zero.'],
['Apuramento do período — campos14–18', 'Indeterminado: faltam componentes e enquadramentos do Modelo A. IVA das vendas não é imposto final a pagar.'],
['Apuramento nesta execução de vendas — campos14–18', 'Indeterminado nesta execução isolada de01/02. Consulte o bloco separado de aquisições e apuramento parcial, quando preenchido. IVA das vendas não é imposto final a pagar.'],
['Origem dos dados','Projecção do negócio; não são operações efectivas nem declaração fiscal.'],
['Exemplar de referência',`${modeloAReference.file}; SHA256 ${modeloAReference.sha256}. ${modeloAReference.edition}`],
];
+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.';
+6 -1
View File
@@ -3,10 +3,12 @@ import { investmentFieldOrigin } from './context-inheritance.ts';
import { investmentLandPreparation } from './investment-land-preparation.ts';
import { monthlyIvaInput, monthlyIvaKey, readMonthlyIvaRecord, readMonthlyIvaSettings } from './monthly-iva.ts';
import { modeloAPurchaseReferences } from './iva-modelo-a.ts';
import { acquisitionReportRows } from './iva-acquisitions.ts';
import { fiscalQaRows } from './fiscal-qa-preview.ts';
import { csvCellWithDelimiter } from './csv.js';
import {
restoreCashflowDraftSnapshot,
cashflowCurrency,
projectMonthlyCashflow,
} from './monthly-cashflow.ts';
import {
@@ -613,6 +615,7 @@ export function buildProjectDossierRows(
'Planeado/realizado declarado pelo utilizador',
]);
}
rows.push(...acquisitionReportRows(draft));
if (draft.monthly_iva_snapshot) {
const saved = readMonthlyIvaRecord(String(draft.monthly_iva_snapshot));
let current = false;
@@ -637,7 +640,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,69 @@
<?php
namespace Tests\Feature;
use App\Models\User;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;
use Tests\TestCase;
final class ModeloAAcquisitionsDraftTest extends TestCase
{
use DatabaseTransactions;
public function test_ui_calculated_payload_is_saved_recovered_and_used_as_the_report_source(): 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);
$this->actingAs(User::factory()->create(['email_verified_at' => now()]));
$path = getenv('IVA_ACQUISITION_UI_INPUT') ?: base_path('tests/frontend/fixtures/modelo-a-acquisitions.json');
$draft = json_decode(file_get_contents($path), true, flags: JSON_THROW_ON_ERROR);
$draft['purpose'] = 'simulate_only';
$created = $this->postJson('/api/v1/simulation-drafts', ['personal_snapshot' => [], 'economic_snapshot' => $draft], ['Idempotency-Key' => (string) Str::uuid()])->assertCreated();
$recovered = $this->getJson('/api/v1/simulation-drafts/'.$created->json('data.id'))->assertOk()->json('data.economic_snapshot');
$this->assertSame($draft['modelo_a_snapshot'], $recovered['modelo_a_snapshot']);
$this->assertSame($draft['context_opportunity'], $recovered['context_opportunity']);
if (getenv('IVA_ACQUISITION_UI_RECOVERED_OUT')) file_put_contents(getenv('IVA_ACQUISITION_UI_RECOVERED_OUT'), json_encode($recovered, JSON_THROW_ON_ERROR | JSON_UNESCAPED_UNICODE));
}
public function test_calculated_acquisitions_round_trip_in_private_draft_and_other_owner_cannot_read(): 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]);
$fingerprint = 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;
$actor = User::factory()->create(['email_verified_at' => now()]);
$this->actingAs($actor);
$draft = json_decode(file_get_contents(base_path('tests/frontend/fixtures/modelo-a-acquisitions.json')), true, flags: JSON_THROW_ON_ERROR);
$draft['purpose'] = 'simulate_only';
$created = $this->postJson('/api/v1/simulation-drafts', ['personal_snapshot' => [], 'economic_snapshot' => $draft], ['Idempotency-Key' => (string) Str::uuid()])->assertCreated();
$id = $created->json('data.id');
$this->assertNotEmpty($id);
$saved = $this->getJson('/api/v1/simulation-drafts/'.$id)->assertOk();
$economic = $saved->json('data.economic_snapshot');
$this->assertSame($draft['modelo_a_snapshot'], $economic['modelo_a_snapshot']);
$store = json_decode($economic['modelo_a_snapshot'], true, flags: JSON_THROW_ON_ERROR);
$first = $store['books']['opportunity:309'];
$this->assertSame('144.0048', $first['result']['periods'][0]['fields'][17]);
$store['books']['opportunity:310'] = $first;
$store['books']['opportunity:310']['rows'][0]['net'] = '200';
$store['books']['opportunity:310']['rows'][0]['vat'] = '32';
$economic['context_opportunity'] = 'opportunity:310';
$economic['modelo_a_snapshot'] = json_encode($store, JSON_THROW_ON_ERROR | JSON_UNESCAPED_UNICODE);
$this->putJson('/api/v1/simulation-drafts/'.$id, ['personal_snapshot' => [], 'economic_snapshot' => $economic], ['If-Match' => $saved->headers->get('ETag')])->assertOk();
$recovered = $this->getJson('/api/v1/simulation-drafts/'.$id)->assertOk()->json('data.economic_snapshot');
$this->assertSame($economic['modelo_a_snapshot'], $recovered['modelo_a_snapshot']);
$this->assertSame($first, json_decode($recovered['modelo_a_snapshot'], true)['books']['opportunity:309']);
if (getenv('IVA_ACQUISITION_RECOVERED_OUT')) {
// The original calculated opportunity, recovered through the real private API, is the report source.
$economic['context_opportunity'] = 'opportunity:309';
file_put_contents(getenv('IVA_ACQUISITION_RECOVERED_OUT'), json_encode($economic, JSON_THROW_ON_ERROR | JSON_UNESCAPED_UNICODE));
}
$this->actingAs(User::factory()->create(['email_verified_at' => now()]));
$this->getJson('/api/v1/simulation-drafts/'.$id)->assertNotFound();
$this->assertSame($fingerprint, 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);
}
}
@@ -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,15 @@
{
"project_title": "SYNTHETIC QA — aquisições e apuramento parcial Modelo A",
"currency": "MZN",
"context_opportunity": "opportunity:309",
"context_applicability_snapshot": "{\"version\":1,\"context_revision\":13,\"consumer_opportunity\":\"opportunity:309\",\"opportunity_choices\":[{\"opportunity\":{\"key\":\"opportunity:309\",\"title\":\"SYNTHETIC QA — segunda oportunidade Manica\"}}],\"path\":{\"steps\":[]}}",
"business_plan_snapshot": "{\"version\":17,\"plan\":{\"openingCash\":\"0\",\"monthlyCollections\":\"0\",\"monthlyVariableCostPayments\":\"0\",\"monthlyInventoryPurchases\":\"0\",\"monthlyOperatingExpenses\":\"0\",\"monthlyOperatingPayments\":\"0\",\"monthlyPayrollExpense\":\"0\",\"monthlyPayrollPayments\":\"0\",\"assetInvestment\":\"0\",\"inventory\":\"0\",\"receivables\":\"0\",\"payables\":\"0\",\"equity\":\"0\",\"shareholderLoans\":\"0\",\"bankLoanProceeds\":\"0\",\"grants\":\"0\",\"assetDepreciation\":\"0\",\"monthlyInterestExpense\":\"0\",\"monthlyInterestPayment\":\"0\",\"monthlyPrincipalRepayment\":\"0\",\"currency\":\"MZN\",\"periods\":4,\"depreciationLifeMonths\":60,\"products\":[{\"label\":\"SYNTHETIC QA\",\"monthlyUnits\":\"1\",\"unitPrice\":\"1000.03\",\"unitVariableCost\":\"0\",\"monthlyUnitsByPeriod\":[\"1\",\"2\",\"0\",\"\"]}],\"inventoryPurchaseLeadMonths\":0,\"inventoryPurchaseSchedule\":null,\"useInventoryPurchaseSchedule\":false,\"useProductInventorySchedule\":false,\"useSupplierPaymentTerms\":false,\"monthlyVariableCostPaymentsByPeriod\":null,\"salesCollectionLagMonths\":null,\"salesCollectionPercent\":\"100\",\"openingReceivablesCollectionMonth\":null,\"openingReceivablesCollectionPercent\":\"100\",\"openingReceivablesInstallments\":[],\"openingPayablesInstallments\":[],\"salesRecoverySchedule\":[]}}",
"monthly_iva_settings": "{\"start\":\"2026-01\",\"basis\":\"net\",\"declared\":true,\"auto\":false}",
"monthly_iva_snapshot": "{\"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},\"execution\":{\"id\":\"06566910-9672-4176-804d-aad8350417cb\",\"status\":\"completed\",\"version_no\":2,\"result_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\":{\"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},\"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\"}],\"cash_treatment\":\"IVA separado da tesouraria; não é imposto final a pagar.\"}}}}",
"province_id": "cf5e81f1-ffbc-535d-b1b6-b44b445c4421",
"district_id": "",
"selection_labels": {
"province_id": "Manica"
},
"modelo_a_snapshot": "{\"version\":1,\"books\":{\"opportunity:309\":{\"currency\":\"MZN\",\"taxpayerRegistered\":\"yes\",\"rows\":[{\"id\":\"qa-inventory\",\"field\":\"09\",\"period\":\"2026-01\",\"origin\":\"projected\",\"sourceMonth\":null,\"sourceAmount\":null,\"sourceCurrency\":\"MZN\",\"sourceBasis\":\"\",\"net\":\"100\",\"vat\":\"16\",\"gross\":\"116\",\"documentType\":\"invoice\",\"documentRef\":\"SYNTHETIC QA 001\",\"documentDate\":\"2026-01-10\",\"documentLine\":\"\",\"supplier\":\"SYNTHETIC QA supplier\",\"supplierRegistered\":\"yes\",\"legalDocument\":\"yes\",\"held\":\"yes\",\"domestic\":\"yes\",\"allocation\":\"taxable_only\",\"exclusion\":\"none\",\"integrity\":\"yes\",\"description\":\"SYNTHETIC QA existências\"}],\"periods\":[{\"period\":\"2026-01\",\"complete\":true,\"absent\":[\"03\",\"04\",\"05\",\"06\",\"07\",\"11\",\"12\",\"13\"]},{\"period\":\"2026-02\",\"complete\":true,\"absent\":[\"03\",\"04\",\"05\",\"06\",\"07\",\"11\",\"12\",\"13\"]},{\"period\":\"2026-03\",\"complete\":true,\"absent\":[\"03\",\"04\",\"05\",\"06\",\"07\",\"11\",\"12\",\"13\"]},{\"period\":\"2026-04\",\"complete\":true,\"absent\":[\"03\",\"04\",\"05\",\"06\",\"07\",\"11\",\"12\",\"13\"]}],\"result\":{\"version\":\"modelo-a-internal-acquisitions/1\",\"revision\":1,\"calculatedAt\":\"2026-10-24T12:00:00.000Z\",\"opportunity\":\"opportunity:309\",\"location\":\"Manica\",\"currency\":\"MZN\",\"sourceKey\":\"5009:840f2acff1252a8e\",\"basis\":\"CIVA arts.18(1)(a),18(2) (Lei10/2025),18(3–5),19(1)(a),19(2),20 (Lei22/2022),21(1–3); âmbito interno, afectação exclusiva, mesmo período, sem exclusões ou dedução parcial.\",\"sources\":\"Modelo A DOCX 62c198e9 / v1 63d45fa8; VAT Return.pdf SHA256 9c0b99a810c7db53eb75d63c22ee22fb86a50ba57bd5509041ed80ee10f75741, p.1; CIVA republicado Lei13/2016, PDF14–16; Lei22/2022, art.20; Lei10/2025, arts.18/19, PDF13.\",\"declarations\":{\"taxpayerRegistered\":\"yes\",\"periods\":[{\"period\":\"2026-01\",\"complete\":true,\"absent\":[\"03\",\"04\",\"05\",\"06\",\"07\",\"11\",\"12\",\"13\"]},{\"period\":\"2026-02\",\"complete\":true,\"absent\":[\"03\",\"04\",\"05\",\"06\",\"07\",\"11\",\"12\",\"13\"]},{\"period\":\"2026-03\",\"complete\":true,\"absent\":[\"03\",\"04\",\"05\",\"06\",\"07\",\"11\",\"12\",\"13\"]},{\"period\":\"2026-04\",\"complete\":true,\"absent\":[\"03\",\"04\",\"05\",\"06\",\"07\",\"11\",\"12\",\"13\"]}]},\"rows\":[{\"id\":\"qa-inventory\",\"net\":\"100.00\",\"vat\":\"16.00\",\"gross\":\"116.00\",\"deductible\":\"16.00\",\"reason\":\"Dedução projectada sob pressupostos explícitos; não certifica factura ou declaração.\"}],\"inputs\":[{\"id\":\"qa-inventory\",\"field\":\"09\",\"period\":\"2026-01\",\"origin\":\"projected\",\"sourceMonth\":null,\"sourceAmount\":null,\"sourceCurrency\":\"MZN\",\"sourceBasis\":\"\",\"net\":\"100\",\"vat\":\"16\",\"gross\":\"116\",\"documentType\":\"invoice\",\"documentRef\":\"SYNTHETIC QA 001\",\"documentDate\":\"2026-01-10\",\"documentLine\":\"\",\"supplier\":\"SYNTHETIC QA supplier\",\"supplierRegistered\":\"yes\",\"legalDocument\":\"yes\",\"held\":\"yes\",\"domestic\":\"yes\",\"allocation\":\"taxable_only\",\"exclusion\":\"none\",\"integrity\":\"yes\",\"description\":\"SYNTHETIC QA existências\"}],\"periods\":[{\"period\":\"2026-01\",\"fields\":{\"10\":\"0.00\",\"11\":\"0.00\",\"12\":\"0.00\",\"13\":\"0.00\",\"14\":\"1000.03\",\"15\":\"16.00\",\"16\":\"160.0048\",\"17\":\"144.0048\",\"18\":\"0.00\",\"01\":\"1000.03\",\"02\":\"160.0048\",\"03\":\"0.00\",\"04\":\"0.00\",\"05\":\"0.00\",\"06\":\"0.00\",\"07\":\"0.00\",\"08\":\"0.00\",\"09\":\"16.00\"},\"reasons\":[]},{\"period\":\"2026-02\",\"fields\":{\"10\":\"0.00\",\"11\":\"0.00\",\"12\":\"0.00\",\"13\":\"0.00\",\"14\":\"2000.06\",\"15\":\"0.00\",\"16\":\"320.0096\",\"17\":\"320.0096\",\"18\":\"0.00\",\"01\":\"2000.06\",\"02\":\"320.0096\",\"03\":\"0.00\",\"04\":\"0.00\",\"05\":\"0.00\",\"06\":\"0.00\",\"07\":\"0.00\",\"08\":\"0.00\",\"09\":\"0.00\"},\"reasons\":[]},{\"period\":\"2026-03\",\"fields\":{\"10\":\"0.00\",\"11\":\"0.00\",\"12\":\"0.00\",\"13\":\"0.00\",\"14\":\"0.00\",\"15\":\"0.00\",\"16\":\"0.00\",\"17\":\"0.00\",\"18\":\"0.00\",\"01\":\"0.00\",\"02\":\"0\",\"03\":\"0.00\",\"04\":\"0.00\",\"05\":\"0.00\",\"06\":\"0.00\",\"07\":\"0.00\",\"08\":\"0.00\",\"09\":\"0.00\"},\"reasons\":[]},{\"period\":\"2026-04\",\"fields\":{\"10\":\"0.00\",\"11\":\"0.00\",\"12\":\"0.00\",\"13\":\"0.00\",\"14\":null,\"15\":\"0.00\",\"16\":null,\"17\":null,\"18\":null,\"01\":null,\"02\":null,\"03\":\"0.00\",\"04\":\"0.00\",\"05\":\"0.00\",\"06\":\"0.00\",\"07\":\"0.00\",\"08\":\"0.00\",\"09\":\"0.00\"},\"reasons\":[\"01/02: vendas ou IVA mensal em falta, execução incompleta ou desactualizada.\"]}]}}}}"
}
@@ -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})}};
}
+78
View File
@@ -0,0 +1,78 @@
import assert from 'node:assert/strict';
import test from 'node:test';
import fs from 'node:fs';
import {closureFixture} from './fixtures/simulator-closure-fixture.mjs';
import {acquisitionBook,acquisitionCurrent,acquisitionExcludedFields,acquisitionPlanReferences,acquisitionReportRows,calculateAcquisitions,evaluateAcquisition,newAcquisition,reuseAcquisitions,saveAcquisitionBook} from '../../resources/js/lib/iva-acquisitions.ts';
import {createProjectDossierReport} from '../../resources/js/lib/project-dossier-report.ts';
import {serializeBusinessPlanSnapshot,restoreBusinessPlanSnapshot} from '../../resources/js/lib/business-financial-plan.ts';
export function acquisitionsFixture(){
const draft=closureFixture().draft;
draft.project_title='SYNTHETIC QA — aquisições e apuramento parcial Modelo A';
draft.province_id='cf5e81f1-ffbc-535d-b1b6-b44b445c4421';draft.district_id='';draft.selection_labels={province_id:'Manica'};
const book={currency:'MZN',taxpayerRegistered:'yes',rows:[{...newAcquisition('qa-inventory','2026-01','MZN'),net:'100',vat:'16',gross:'116',documentType:'invoice',documentRef:'SYNTHETIC QA 001',documentDate:'2026-01-10',supplier:'SYNTHETIC QA supplier',supplierRegistered:'yes',legalDocument:'yes',held:'yes',domestic:'yes',allocation:'taxable_only',exclusion:'none',integrity:'yes',description:'SYNTHETIC QA existências'}],periods:['2026-01','2026-02','2026-03','2026-04'].map(period=>({period,complete:true,absent:[...acquisitionExcludedFields]}))};
return {draft,book};
}
test('a recorded purchase does not establish VAT, legal document or deduction',()=>{
const row=newAcquisition('a','2026-01','MZN');row.net='100';
assert.equal(evaluateAcquisition(row,'MZN','yes').deductible,null);
row.vat='16';let r=evaluateAcquisition(row,'MZN','yes');assert.equal(r.gross,'116.00');assert.equal(r.deductible,null);
row.gross='117';assert.match(evaluateAcquisition(row,'MZN','yes').reason,/corresponder/);
});
test('eligibility handles exclusions, mixed use, document, supplier, date and currency without inventing rules',()=>{
const row=acquisitionsFixture().book.rows[0];
assert.equal(evaluateAcquisition(row,'MZN').deductible,null);
assert.equal(evaluateAcquisition(row,'MZN','yes').deductible,'16.00');
for(const patch of [{documentRef:''},{supplierRegistered:''},{held:''},{allocation:'mixed'},{exclusion:'special'},{documentDate:'2026-02-10'},{documentDate:'2026-99-10'},{domestic:'no'},{sourceCurrency:'USD'}]) assert.equal(evaluateAcquisition({...row,...patch},'MZN','yes').deductible,null,JSON.stringify(patch));
for(const patch of [{supplierRegistered:'no'},{allocation:'no_right'},{exclusion:'excluded'},{integrity:'no'}]) assert.equal(evaluateAcquisition({...row,...patch},'MZN','yes').deductible,'0.00');
assert.equal(evaluateAcquisition({...row,origin:'effective'},'MZN','yes').deductible,'16.00');
assert.equal(evaluateAcquisition({...row,net:'100',vat:'0',gross:'100',documentRef:''},'MZN','yes').deductible,'0.00');
});
test('duplicates, ambiguous opportunity and absent declarations are not silently accepted',()=>{
const {draft,book}=acquisitionsFixture();
book.rows.push({...book.rows[0],id:'duplicate'});assert.throws(()=>calculateAcquisitions(draft,book),/repetido/);
assert.throws(()=>acquisitionBook({...draft,context_opportunity:'',context_applicability_snapshot:JSON.stringify({opportunity_choices:[{},{}]})}),/Escolha a oportunidade/);
const missing=acquisitionsFixture();missing.book.periods[0].absent=[];missing.book.result=calculateAcquisitions(missing.draft,missing.book);missing.draft.modelo_a_snapshot=saveAcquisitionBook(missing.draft,missing.book);
assert.match(JSON.stringify(acquisitionReportRows(missing.draft)),/nenhum/);
assert.equal(missing.book.result.periods[0].fields['17'],null);
});
test('08–10, formulas14–18, decimal precision, missing parcels and explicit absence',()=>{
const {draft,book}=acquisitionsFixture();
book.rows.push({...book.rows[0],id:'asset',field:'08',documentLine:'2',net:'200',vat:'32',gross:'232'},{...book.rows[0],id:'service',field:'10',documentLine:'3',net:'50',vat:'8',gross:'58'});
const result=calculateAcquisitions(draft,book);const f=result.periods[0].fields;
assert.equal(f['08'],'32.00');assert.equal(f['09'],'16.00');assert.equal(f['10'],'8.00');assert.equal(f['14'],'1000.03');assert.equal(f['15'],'56.00');assert.equal(f['16'],'160.0048');assert.equal(f['17'],'104.0048');assert.equal(f['18'],'0.00');
assert.equal(result.periods[2].fields['17'],'0.00');assert.equal(result.periods[3].fields['17'],null);
book.periods[0].absent=[];const incomplete=calculateAcquisitions(draft,book).periods[0];for(const field of ['14','15','16','17','18'])assert.equal(incomplete.fields[field],null);
book.periods[0].absent=[...acquisitionExcludedFields];book.periods[0].complete=false;assert.equal(calculateAcquisitions(draft,book).periods[0].fields['15'],null);
const credit=acquisitionsFixture();credit.book.rows[0]={...credit.book.rows[0],net:'2000',vat:'320',gross:'2320'};assert.equal(calculateAcquisitions(credit.draft,credit.book).periods[0].fields['18'],'159.9952');
assert.throws(()=>calculateAcquisitions({...draft,imports:'5'},book),/importa/);
});
test('reuse keeps source values unclassified, preserves deliberate changes and detects source drift',()=>{
const {draft}=acquisitionsFixture();let book=reuseAcquisitions(draft,acquisitionBook(draft));assert.equal(book.rows[0].sourceAmount,'0.00');assert.equal(book.rows[0].net,'');assert.equal(book.rows[0].sourceBasis,'');
book.rows[0].net='123';book=reuseAcquisitions(draft,book);assert.equal(book.rows[0].net,'123');assert.equal(book.rows.length,4);
const plan=restoreBusinessPlanSnapshot(draft.business_plan_snapshot);plan.monthlyInventoryPurchases='1';const changed={...draft,business_plan_snapshot:serializeBusinessPlanSnapshot(plan)};assert.throws(()=>calculateAcquisitions(changed,book),/plano mudaram/);
});
test('asset and operating budgets can be reused explicitly without becoming VAT or deductions',()=>{
const {draft,book}=acquisitionsFixture();const plan=restoreBusinessPlanSnapshot(draft.business_plan_snapshot);plan.assetInvestment='200';plan.monthlyOperatingExpenses='50';draft.business_plan_snapshot=serializeBusinessPlanSnapshot(plan);
const refs=acquisitionPlanReferences(draft);assert.equal(refs.find(r=>r.kind==='asset').amount,'200');assert.equal(refs.find(r=>r.key==='operating:1').amount,'50');
const row={...newAcquisition('asset-reference','2026-01','MZN'),field:'08',sourceField:'asset',sourceMonth:1,sourceAmount:'200',sourceBasis:'net',net:'200'};book.rows=[row];assert.equal(calculateAcquisitions(draft,book).rows[0].deductible,null);
row.vat='32';assert.equal(calculateAcquisitions(draft,book).rows[0].deductible,null);
assert.equal(calculateAcquisitions(draft,book).rows[0].gross,'232.00');
});
test('order month is retained as origin and never forces invoice or deduction month',()=>{
const {draft}=acquisitionsFixture();const plan=restoreBusinessPlanSnapshot(draft.business_plan_snapshot);plan.useInventoryPurchaseSchedule=true;plan.inventoryPurchaseSchedule=[{amount:'100',leadMonths:0},{amount:'0',leadMonths:0},{amount:'0',leadMonths:0},{amount:'0',leadMonths:0}];draft.business_plan_snapshot=serializeBusinessPlanSnapshot(plan);
let book=reuseAcquisitions(draft,acquisitionBook(draft));book.taxpayerRegistered='yes';book.periods=book.periods.map(p=>({...p,complete:true,absent:[...acquisitionExcludedFields]}));
book.rows=book.rows.map(row=>({...row,net:row.sourceAmount,vat:'0',sourceBasis:'net'}));
book.rows[0]={...book.rows[0],...acquisitionsFixture().book.rows[0],id:'plan-1',sourceMonth:1,sourcePeriod:'2026-01',sourceAmount:'100.00',sourceBasis:'net',period:'2026-02',documentDate:'2026-02-10'};
const result=calculateAcquisitions(draft,book);assert.equal(result.rows[0].deductible,'16.00');assert.equal(result.periods[0].fields['09'],'0.00');assert.equal(result.periods[1].fields['09'],'16.00');assert.equal(result.inputs[0].sourcePeriod,'2026-01');
});
test('second opportunity changes preserve first book, frozen results and report context after recovery',()=>{
const {draft,book}=acquisitionsFixture();draft.context_opportunity='opportunity:1';book.result=calculateAcquisitions(draft,book);draft.modelo_a_snapshot=saveAcquisitionBook(draft,book);const first=JSON.stringify(acquisitionBook(draft));
draft.context_opportunity='opportunity:309';assert.equal(acquisitionBook(draft).rows.length,0);const second=acquisitionsFixture().book;second.rows[0].vat='32';second.rows[0].net='200';second.rows[0].gross='232';second.result=calculateAcquisitions(draft,second);draft.modelo_a_snapshot=saveAcquisitionBook(draft,second);
const recovered=JSON.parse(JSON.stringify(draft));assert.equal(acquisitionCurrent(recovered,acquisitionBook(recovered)),true);assert.equal(acquisitionBook(recovered).result.periods[0].fields['17'],'128.0048');
assert.equal(JSON.stringify(acquisitionBook({...draft,context_opportunity:'opportunity:1'})),first);
const report=JSON.stringify(createProjectDossierReport(recovered,{selectedSections:new Set(['finance'])}));assert.match(report,/128\.0048/);assert.match(report,/opportunity:309/);assert.doesNotMatch(report,/opportunity:1;/);
const old=JSON.stringify(second.result);second.rows[0].vat='33';draft.modelo_a_snapshot=saveAcquisitionBook(draft,second);assert.equal(acquisitionCurrent(draft,second),false);assert.equal(JSON.stringify(second.result),old);assert.match(JSON.stringify(acquisitionReportRows(draft)),/Histórico desactualizado/);
assert.throws(()=>acquisitionBook({modelo_a_snapshot:'broken'}));
});
if(process.env.IVA_ACQUISITION_FIXTURE_OUT){const {draft,book}=acquisitionsFixture();book.result=calculateAcquisitions(draft,book,'2026-10-24T12:00:00.000Z');draft.modelo_a_snapshot=saveAcquisitionBook(draft,book);fs.writeFileSync(process.env.IVA_ACQUISITION_FIXTURE_OUT,JSON.stringify(draft,null,2));}
@@ -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,
);