Compare commits

...
Author SHA1 Message Date
nala dacda11c07 Record joint L1 homologation delivery 490527b9 and verified preservation 2026-10-24 14:32:35 +02:00
nala 490527b92c Record joint L1 QA candidate, verified Git bundle and integration preview
tests / ci (push) Canceled after 0s
2026-10-24 14:22:15 +02:00
nala 4a35d63cd3 Complete G2B social QA and prepare joint L1 delivery with compatible rollback 2026-10-24 14:21:21 +02:00
nala 0e644b5524 Record G2A QA candidate and reconciled L1 closure counts 2026-10-24 14:04:45 +02:00
nala 675cf38f67 Implement G2A health exemptions in QA and reconcile L1 branch inventory 2026-10-24 14:04:18 +02:00
nala 89ab288334 Record L1 grouped QA candidate and verified final export evidence 2026-10-24 13:54:42 +02:00
nala 5030116ee5 Group L1 coverage and implement documented service exemptions in QA 2026-10-24 13:54:32 +02:00
nala 8719e916ea docs: record rational VAT QA candidate and open form dependency 2026-10-24 13:20:52 +02:00
nala 689e7cbe8b feat: preserve exact included VAT fractions and enumerate L1 remainder 2026-10-24 13:20:32 +02:00
nala d89912e579 docs: record IVA L1 QA candidate and explicit remaining scope 2026-10-24 13:00:26 +02:00
nala 544068e8cc feat: demonstrate scoped IVA operations L1 in QA 2026-10-24 12:59:37 +02:00
nala e8c39a1b54 docs: receipt for installed Modelo A acquisitions and totals 2026-10-24 12:19:34 +02:00
nala 1655f57e69 docs: register Modelo A acquisitions QA candidate and evidence
tests / ci (push) Canceled after 0s
2026-10-24 12:14:21 +02:00
nala d18ff0f049 feat: implement opportunity-scoped Modelo A acquisitions in QA 2026-10-24 12:13:57 +02:00
nala 355656b5c8 docs: receipt for installed simulator S1-S3 closure in homologation 2026-10-24 11:41:57 +02:00
nala cd10cccbf4 docs: close S3 monthly PDF review and mark simulator candidate ready for integration
tests / ci (push) Canceled after 0s
2026-10-24 11:37:09 +02:00
nala d044ef1efc docs: record S1 and S2 QA results and directed S3 file checks 2026-10-24 11:30:59 +02:00
nala 65b87819a0 fix: preserve cashflow currency and recover admitted monthly IVA executions in QA 2026-10-24 11:29:15 +02:00
nala be62783b98 docs: delimit simulator closure to two code gaps and monthly export proof 2026-10-24 11:16:42 +02:00
41 changed files with 5873 additions and 45 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'],
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,512 @@
{
"status": "installed_delta_verified_partial_L1",
"candidate": "4a35d63cd3535195e4e2a37740b2f52813ff1da9",
"base_remote_main": "1655f57e698fef4099c2886069a79cd1c920f792",
"fetch": "completed;noforce;localmainnotused",
"bundle": {
"path": "outputs\\iva-l1-g2-social-20261024\\l1-joint-4a35d63c.bundle",
"candidate": "4a35d63cd3535195e4e2a37740b2f52813ff1da9",
"base": "1655f57e698fef4099c2886069a79cd1c920f792",
"sha256": "7e23cabda77c8e7b117091a08613436f02c32a9d23d023d4d030c9041eb43146",
"bytes": 166937,
"verified": true,
"content": "FunctionalQAcommit4a35d63c+historyfromorigin/main;receiptmetadatafinalizedseparately;noactivepush"
},
"coverage": {
"initial_QA": "544068e8/d89912e5:10conditionaltreatments",
"rational_QA": "689e7cbe/8719e916;exactincluded5%andreaders",
"G1": "5030116e/89ab2883:4branches/4treatments",
"G2A": "675cf38f/0e644b55:5branches/5treatments",
"G2B": {
"AUD-SIM-L1-P009": [
"social_assistance_public",
"social_assistance_nonprofit"
],
"AUD-SIM-L1-P011": [
"social_care_public",
"social_care_nonprofit"
],
"AUD-SIM-L1-P012": [
"activity_facilities_public",
"activity_facilities_nonprofit"
]
},
"conditional_treatments_total": 25,
"indeterminate_options": 2,
"QA_covered_inventory_IDs": [
"AUD-SIM-L1-P003",
"AUD-SIM-L1-P004",
"AUD-SIM-L1-P005",
"AUD-SIM-L1-P006",
"AUD-SIM-L1-P007",
"AUD-SIM-L1-P009",
"AUD-SIM-L1-P011",
"AUD-SIM-L1-P012",
"AUD-SIM-L1-P017",
"AUD-SIM-L1-P018",
"AUD-SIM-L1-P029",
"AUD-SIM-L1-P030"
],
"pending_functional_IDs": [
"AUD-SIM-L1-P002",
"AUD-SIM-L1-P008",
"AUD-SIM-L1-P010",
"AUD-SIM-L1-P013",
"AUD-SIM-L1-P014",
"AUD-SIM-L1-P015",
"AUD-SIM-L1-P016",
"AUD-SIM-L1-P019",
"AUD-SIM-L1-P020",
"AUD-SIM-L1-P021",
"AUD-SIM-L1-P022",
"AUD-SIM-L1-P023",
"AUD-SIM-L1-P024",
"AUD-SIM-L1-P025",
"AUD-SIM-L1-P026",
"AUD-SIM-L1-P027",
"AUD-SIM-L1-P028",
"AUD-SIM-L1-P031",
"AUD-SIM-L1-P032",
"AUD-SIM-L1-P033",
"AUD-SIM-L1-P034",
"AUD-SIM-L1-P035",
"AUD-SIM-L1-P036",
"AUD-SIM-L1-P037",
"AUD-SIM-L1-P038",
"AUD-SIM-L1-P039",
"AUD-SIM-L1-P040",
"AUD-SIM-L1-P041",
"AUD-SIM-L1-P042",
"AUD-SIM-L1-P043",
"AUD-SIM-L1-P044",
"AUD-SIM-L1-P045",
"AUD-SIM-L1-P046",
"AUD-SIM-L1-P047",
"AUD-SIM-L1-P048",
"AUD-SIM-L1-P049",
"AUD-SIM-L1-P050",
"AUD-SIM-L1-P051",
"AUD-SIM-L1-P052",
"AUD-SIM-L1-P053",
"AUD-SIM-L1-P054",
"AUD-SIM-L1-P055",
"AUD-SIM-L1-P056",
"AUD-SIM-L1-P057",
"AUD-SIM-L1-P058",
"AUD-SIM-L1-P059",
"AUD-SIM-L1-P060",
"AUD-SIM-L1-P061",
"AUD-SIM-L1-P062",
"AUD-SIM-L1-P063",
"AUD-SIM-L1-P064",
"AUD-SIM-L1-P065",
"AUD-SIM-L1-P066",
"AUD-SIM-L1-P067",
"AUD-SIM-L1-P068",
"AUD-SIM-L1-P069",
"AUD-SIM-L1-P070",
"AUD-SIM-L1-P071",
"AUD-SIM-L1-P072",
"AUD-SIM-L1-P073",
"AUD-SIM-L1-P074",
"AUD-SIM-L1-P075",
"AUD-SIM-L1-P076",
"AUD-SIM-L1-P077",
"AUD-SIM-L1-P078",
"AUD-SIM-L1-P079",
"AUD-SIM-L1-P080",
"AUD-SIM-L1-P081",
"AUD-SIM-L1-P082",
"AUD-SIM-L1-P083",
"AUD-SIM-L1-P084",
"AUD-SIM-L1-P085",
"AUD-SIM-L1-P086",
"AUD-SIM-L1-P087",
"AUD-SIM-L1-P088",
"AUD-SIM-L1-P089",
"AUD-SIM-L1-P090",
"AUD-SIM-L1-P091",
"AUD-SIM-L1-P092",
"AUD-SIM-L1-P093",
"AUD-SIM-L1-P094",
"AUD-SIM-L1-P095",
"AUD-SIM-L1-P096",
"AUD-SIM-L1-P097",
"AUD-SIM-L1-P098",
"AUD-SIM-L1-P099",
"AUD-SIM-L1-P100",
"AUD-SIM-L1-P101",
"AUD-SIM-L1-P102",
"AUD-SIM-L1-P103",
"AUD-SIM-L1-P104",
"AUD-SIM-L1-P105",
"AUD-SIM-L1-P106",
"AUD-SIM-L1-P107",
"AUD-SIM-L1-P108",
"AUD-SIM-L1-P109",
"AUD-SIM-L1-P110",
"AUD-SIM-L1-P111",
"AUD-SIM-L1-P112",
"AUD-SIM-L1-P113",
"AUD-SIM-L1-P114",
"AUD-SIM-L1-P115",
"AUD-SIM-L1-P116",
"AUD-SIM-L1-P117",
"AUD-SIM-L1-P118",
"AUD-SIM-L1-P119",
"AUD-SIM-L1-P120",
"AUD-SIM-L1-P121",
"AUD-SIM-L1-P122",
"AUD-SIM-L1-P123",
"AUD-SIM-L1-P124",
"AUD-SIM-L1-P125",
"AUD-SIM-L1-P126",
"AUD-SIM-L1-P127",
"AUD-SIM-L1-P128",
"AUD-SIM-L1-P129",
"AUD-SIM-L1-P130",
"AUD-SIM-L1-P131",
"AUD-SIM-L1-P132",
"AUD-SIM-L1-P133"
],
"pending_documentary_IDs": [
"AUD-SIM-L1-P001"
]
},
"runtime_delta": [
{
"path": "resources/js/components/simulation-monthly-iva.tsx",
"sha256_LF": "27e9f7b0a6cd011d6278d5bf507c8df0a290432006d795e9f7bf03b05fc6b350"
},
{
"path": "resources/js/components/simulation-operations.tsx",
"sha256_LF": "ae8fafbff339c4b52bae11ad479caf0fb451e5543e59bfbf15becf9aab4c5d27"
},
{
"path": "resources/js/lib/fiscal-rational.ts",
"sha256_LF": "91234efa465a8660f71a56e48d280a602c56ecf2ccce9cf7a8d91db513a3ffd3"
},
{
"path": "resources/js/lib/iva-acquisitions.ts",
"sha256_LF": "846dbd2d2dbdd54ffcddae8623eeaa2ae4f7686c6e2db05590ffa893d3ffbfb0"
},
{
"path": "resources/js/lib/iva-operations.ts",
"sha256_LF": "fb41bf3d6cf14ee65bf242f5a2504f9d4758c8d87bb4c4e9eda4822f74aa4d7f"
},
{
"path": "resources/js/lib/project-dossier.ts",
"sha256_LF": "5a6ea8a8c7261fa1d24cca56e92617cd66f72bd027dbafec22d051e686e927d7"
}
],
"schema_routes_permissions_dependencies_changed": false,
"database_imports_migrations_seeds": [],
"server_fiscal_rule_activation": false,
"activation": {
"method": "ExistingGitFF/merge+build+webhook;no new mandatory flag",
"preflight": [
"Re-fetchorigin/main;reconcileconcurrentchangeswithoutforce",
"Confirmactualservedrevisionandvalidbackup/restoreresultthroughauthenticatedexistingprocess",
"Usecompiledassets;excludeoutputs/syntheticdata/publichot/secrets",
"No globalmigration,seed,taxpublication or datareplacement"
],
"installed_delta_only": [
"servedrevisionandmanifesthash",
"simulatorusesnewasset",
"newoptions/readerscontextvisible;existingdraftrecoverable",
"prioracceptedproofsreused;nosyntheticwritesinactive"
],
"completed": true,
"database_migrations": 0,
"content_imports": 0
},
"reversal": {
"preferred": "ForwardGitcompatibilitycontainmentcommitusingpreparedreadonlypatch,notDBrestore;revertcontainmentcommitafterfix",
"patch": {
"patch": "docs\\investmoz\\simulations\\l1-joint-readonly-rollback-20261024.patch",
"patch_sha256": "d25a81f025ffeeb5869161404eb23b9762d87e07b4a820c14b63d29359d0db0e",
"component_before_sha256": "37676ccb4302e5fdf5dceb58291809f120ea204c2a8c6a59ede9585f8753aa73",
"git_apply_check": true,
"live_component_changed": false,
"purpose": "Operational compatibility rollback: suspend edits/calculations in L1 panel only;keep catalogue,readers,history,API,otherforms and reports;no DBrestore or broad path disable"
},
"QA_UI": "L1inputs/calculationdisabled;details/result600MZN/revision3/readers/reportsremain;otherforms/APIpermissionsunchanged",
"limitations": "UIoperationalcontainment,notAPIauthorizationchange;fullrollbackto1655wouldhidenewL1readersandisnotpreferred"
},
"destination_precheck": {
"public_revision_url": "https://teste-investidor.tempo.co.mz/build/revision.json",
"result": "Authenticateddestinationverified;existingdeliveryHTTPclient200;defaulturllib403/Cloudflare1010",
"served_revision_verified_this_pass": true,
"protected_installer_or_current_backup_verified": true,
"historical_initial_result": "HTTP403 before client diagnostic",
"diagnostic": [
{
"request": "default",
"status": 403,
"headers": {
"Content-Type": "text/plain; charset=UTF-8",
"Server": "cloudflare",
"CF-RAY": "a4f8f78b4cb51d20-JNB"
},
"body_sha256": "2938e9f1284180959e33ab1718d0793a72ff6e4cdb8108c34dcd14e69446de5c",
"error_excerpt": "error code: 1010\n"
},
{
"request": "existing_delivery",
"status": 200,
"headers": {
"Content-Type": "application/json",
"Server": "cloudflare",
"CF-RAY": "a4f8f78bdf3c3575-JNB"
},
"body_sha256": "ad3f581baf0ef17706246b1f87c5fd0fd3659b14c6ed8f7661a19d68d838d40b",
"commit": "1655f57e698fef4099c2886069a79cd1c920f792"
}
],
"cloudflare_reference": "https://developers.cloudflare.com/support/troubleshooting/http-status-codes/cloudflare-1xxx-errors/error-1010/",
"conclusion": "Cloudflare1010forurllibdefault;existingdeliveryclient200;specificCloudflarerulenotinspected;noapplicationpermissioncauseorconfigurationchangerequired"
},
"excluded": [
"P001mapping",
"120unimplementedbranches",
"newtaxes",
"IRPC/IRPS",
"statistics/AI",
"syntheticQArecordsandruntimefiles"
],
"delivery_performed": true,
"integration_preview": {
"remote": "1655f57e698fef4099c2886069a79cd1c920f792",
"merge_tree": "2284cf875e1b2ee6dd34ae5d182f57a3c194bf22",
"candidate_tree_equal": true,
"fast_forward_possible": true,
"conflicts": false,
"main_not_moved": true,
"scope": "Historicalpreparationpreview,beforeauthorizedmainfastforward"
},
"delivery_commit": "490527b92cf1ca18122679440ff3a38933b7e0c2",
"previous_main": "1655f57e698fef4099c2886069a79cd1c920f792",
"installation": {
"authenticated_preflight": {
"installed": {
"status": "installed",
"commit": "1655f57e698fef4099c2886069a79cd1c920f792",
"release": "/var/www/investmoz-homologacao/releases/1655f57e698fef4099c2886069a79cd1c920f792-fe9d3b11",
"previous_release": "/var/www/investmoz-homologacao/releases/cd10cccbf45f53836d8bf7b2bd90794d99602cb9-a38feaad",
"database_migrations": 0,
"content_imports": 0
},
"current_link": "/var/www/investmoz-homologacao/releases/1655f57e698fef4099c2886069a79cd1c920f792-fe9d3b11",
"release_revision": {
"commit": "1655f57e698fef4099c2886069a79cd1c920f792",
"branch": "main",
"built_at": "2026-10-24T10:18:11.360273+00:00",
"manifest_sha256": "dfb82481796aaf5826e8a5108575de3807ee787faffa92f38a56bb8d9caf5ce7"
},
"service": "ExecMainStatus=0\nActiveState=inactive\nSubState=dead\n",
"configuration_keys": [
"repository",
"branch",
"target_url",
"legacy_root",
"state_root",
"release_root",
"current_link",
"shared_assets",
"php",
"schema_contract",
"ci_environment",
"postgis_image",
"test_database",
"qa_port",
"retention",
"webhook_secret_file"
],
"safe_paths": {
"repository": "nala/Investidor-V2",
"release_root": "/var/www/investmoz-homologacao/releases",
"postgis_image": "postgis/postgis@sha256:01a6a70e41e6c4467c8f55f6063555ed72db2d6662cd0d571040d42eadaeb6f6"
},
"pipeline_sha256": "adec1c17654f3a8658b83b134f1e75f95391b235738552721906c3236e388604"
},
"backup": {
"stage": "/var/backups/investmoz-l1-joint-delivery-20261024/backup",
"target": {
"database": "teste_investidor",
"port": 5432
},
"tables": 174,
"files_verified": 369,
"dump_sha256": "6e9a2b0b350b0effe0812e7e5faa5c9c1886611a50c1be5f8b00515d7ef0c5ea",
"files_sha256": "53c170aa33714e78d997790cb7c75d672e9680052f7b2ffa5bccf2b719ca248e",
"restore_verified": false
},
"restore": {
"restore_verified": true,
"tables_verified": 174,
"container_network": "none",
"database": "l1_joint_restore_20261024",
"port": 55441,
"source_dump_sha256": "6e9a2b0b350b0effe0812e7e5faa5c9c1886611a50c1be5f8b00515d7ef0c5ea",
"active_database_writes": 0
},
"installer": {
"service": "ExecMainStatus=0\nActiveState=inactive\nSubState=dead\n",
"installed": {
"status": "installed",
"commit": "490527b92cf1ca18122679440ff3a38933b7e0c2",
"release": "/var/www/investmoz-homologacao/releases/490527b92cf1ca18122679440ff3a38933b7e0c2-d41ae12f",
"previous_release": "/var/www/investmoz-homologacao/releases/1655f57e698fef4099c2886069a79cd1c920f792-fe9d3b11",
"database_migrations": 0,
"content_imports": 0
},
"latest_report": {
"status": "installed",
"commit": "490527b92cf1ca18122679440ff3a38933b7e0c2",
"release": "/var/www/investmoz-homologacao/releases/490527b92cf1ca18122679440ff3a38933b7e0c2-d41ae12f",
"previous_release": "/var/www/investmoz-homologacao/releases/1655f57e698fef4099c2886069a79cd1c920f792-fe9d3b11",
"database_migrations": 0,
"content_imports": 0,
"requested_commits": [
"490527b92cf1ca18122679440ff3a38933b7e0c2"
],
"finished_at": "2026-10-24T12:30:02.029150+00:00"
},
"log_file": "1792844919670368682.log",
"summaries": [
"OK (1 test, 3106 assertions)",
"built in 23.74s",
"OK (59 tests, 317 assertions)",
"# tests 247",
"# pass 247",
"# fail 0"
],
"log_bytes": 100122
},
"protected_preservation": {
"installed": {
"status": "installed",
"commit": "490527b92cf1ca18122679440ff3a38933b7e0c2",
"release": "/var/www/investmoz-homologacao/releases/490527b92cf1ca18122679440ff3a38933b7e0c2-d41ae12f",
"previous_release": "/var/www/investmoz-homologacao/releases/1655f57e698fef4099c2886069a79cd1c920f792-fe9d3b11",
"database_migrations": 0,
"content_imports": 0
},
"runtime_files_verified": 6,
"persistent_files_and_env_verified": 369,
"tables_compared": 174,
"changed_tables": {
"cache": {
"before": {
"rows": 104,
"checksum": "41ddf6abf1cc00b85fb98a5c22da0ee1"
},
"after": {
"rows": 105,
"checksum": "23e7205245a904489ac3c12248d6b1ac"
}
},
"sessions": {
"before": {
"rows": 17,
"checksum": "436718228e6cb875393809c4a84ebd43"
},
"after": {
"rows": 18,
"checksum": "becbef33ee3be158db37e1878aec832b"
}
}
},
"preserved_counts": {
"published_fiches": 29,
"relations": 28,
"geometries": 408
},
"migration_count": 0,
"content_imports": 0,
"comparison_role": "postgres_before_and_after;sameexportedsnapshotbaseline;noapplicationRLSvisibilityinference",
"writes_by_verification": 0
},
"public_delta": {
"status": "installed_delta_verified",
"served_revision": {
"commit": "490527b92cf1ca18122679440ff3a38933b7e0c2",
"branch": "main",
"built_at": "2026-10-24T12:29:59.442400+00:00",
"manifest_sha256": "b9f7c6d05c78754dd1f9efb71bf5ff59cdfaa1167e136a71138c0fd194b19a33"
},
"manifest_hash_matches": true,
"compiled_delta_markers": {
"iva-operations-l1/2": [
"assets/project-dossier-Cqjhibc1.js"
],
"financial_stamp": [
"assets/project-dossier-Cqjhibc1.js"
],
"insurance_stamp": [
"assets/project-dossier-Cqjhibc1.js"
],
"health_assistive": [
"assets/project-dossier-Cqjhibc1.js"
],
"patient_transport": [
"assets/project-dossier-Cqjhibc1.js"
],
"social_assistance_": [
"assets/project-dossier-Cqjhibc1.js"
],
"social_care_": [
"assets/project-dossier-Cqjhibc1.js"
],
"activity_facilities_": [
"assets/project-dossier-Cqjhibc1.js"
],
"nonprofit": [
"assets/project-dossier-Cqjhibc1.js"
],
"exactFields": [
"assets/project-dossier-Cqjhibc1.js"
],
"20/21": [
"assets/project-dossier-Cqjhibc1.js"
]
},
"assets": [
{
"file": "assets/services-DUPANwbE.js",
"status": 200,
"sha256": "cfdd24a5527c693b0a227726babd1a054425c19cc4a0ebe5f215f582ba5bf302"
},
{
"file": "assets/simulation-form-CPZAFZU3.js",
"status": 200,
"sha256": "c6bcf64e60a5eeac0cafe3dc055dc5544162f21942ee08b34d51bc6d52947567"
},
{
"file": "assets/project-dossier-Cqjhibc1.js",
"status": 200,
"sha256": "937fd99184c12b1321abf86bf67a196c6d74db5296304bc7a746536653560e64"
},
{
"file": "assets/project-dossier-document-91ZtwN0x.js",
"status": 200,
"sha256": "73cb4b1b4ea8928de313cfa858896bfc613dfd0adfb091744a78c81cf5f47764"
}
],
"simulator_status": 200,
"simulator_entry": "assets/app-DVti_AMl.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_private_writes": 0,
"method": "Read-only installedrevision/manifest/newcompiledcatalogueandreaders/simulatorentry;acceptedQAproofsreused"
},
"git_method": "Freshfetch+ancestry/hashcheck+branchpush+fastforwardmain;noforce;normalwebhook",
"rollback_activated": false
}
}
@@ -0,0 +1,19 @@
--- a/resources/js/components/simulation-operations.tsx
+++ b/resources/js/components/simulation-operations.tsx
@@ -9,6 +9,8 @@
const save=(next:SalesBook)=>onChange('modelo_a_snapshot',saveSalesBook(draft,next));
const change=(id:string,patch:Partial<SalesOperation>)=>run(()=>save({...book,rows:book.rows.map(r=>r.id===id?{...r,...patch}:r)}));
return <section aria-label="Operações IVA por oportunidade" className="space-y-3 rounded border p-3">
+ <p role="status">Novos cálculos de operações IVA temporariamente suspensos. Resultados guardados e relatórios permanecem disponíveis.</p>
+ <fieldset disabled>
<h4 className="font-semibold">Operações de IVA — enquadramento das vendas</h4>
<p>Oportunidade {String(draft.context_opportunity||'entrada geral')}; moeda original {book.currency||'por indicar'}. Cobertura2026/MZN. Reutilize as vendas e complete apenas o enquadramento. Este bloco classifica as operações; não substitui nem soma novamente a execução mensal geral acima.</p>
<p>Uma empresa pode ter operações com tratamentos diferentes. Os valores são projecções sob pressupostos declarados, sem certificar facturas, reconhecer créditos ou submeter uma declaração à AT.</p>
@@ -36,6 +38,7 @@
{book.periods.map(p=><label className="block" key={p.period}><input type="checkbox" checked={p.complete} onChange={e=>run(()=>save({...book,periods:book.periods.map(v=>v.period===p.period?{...v,complete:e.target.checked}:v)}))}/> {p.period}: incluí todas as operações deste período; lista vazia significa ausência expressamente confirmada.</label>)}
<p>Limites: outras isenções/bases especiais e correspondência do campo06 permanecem por delimitar. IVA incluído a5% conserva a fracção exacta e apresenta um decimal a6casas; a aproximação de apresentação não constitui regra declarativa. O desconhecido nunca é assumido como zero.</p>
<button type="button" disabled={!book.periods.length} className="rounded bg-emerald-800 p-2 text-white disabled:opacity-50" onClick={()=>run(()=>save(recordSales(draft,book)))}>Calcular e guardar operações IVA</button>
+ </fieldset>
{book.result&&<><p role="status">{salesCurrent(draft,book)?'Resultado correspondente às entradas.':'Resultado histórico conservado; reveja as alterações antes de recalcular.'}</p><dl>{salesReportRows(draft).map(([,label,value,origin],index)=><div className="break-inside-avoid border-t py-2" key={index}><dt className="font-semibold">{label}</dt><dd>{value}<small className="block">{origin}</small></dd></div>)}</dl></>}
</section>;
}
@@ -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,408 @@
# Rastreabilidade, utilização e formação
## 24/10/2026 — entrega conjunta L1 instalada e delta conferido na homologação
**Fecho parcial utilizável instalado em `490527b9`**, candidata funcional `4a35d63c`. Conjunto: motor/classificação L1 inicial, fracções exactas e leitores, G1, G2A e G2B. A activação ocorreu pelo Git/webhook/instalador existentes, sem migrações, importações, publicação/activação de regras fiscais de servidor, alteração de permissões ou de consulta pública. Não equivale ao apuramento integral do IVA nem ao fecho de L1.
**Destino e concorrência:** a sessão SSH autenticada confirmou `installed.json`, symlink actual e revisão construída em1655f57e; instalador inactivo. `git fetch origin` fresco confirmou a mesma revisão remota, sem avanço concorrente; ancestralidade e hashes dos seis ficheiros funcionais conferidos. Publicada a branch do conjunto e actualizado `main` por fast-forward1655f57e→490527b9, sem force. Não se repôs o `main` local antigo nem se substituíram alterações concorrentes. A instalação activou a release490527b9-d41ae12f e conservou1655f57e-fe9d3b11 como release anterior.
**Backup/restauro antes da integração:** novo backup consistente, snapshot exportado da base `teste_investidor:5432`,174tabelas,369ficheiros persistentes/configuração verificados; dump SHA256 `6e9a2b0b350b0effe0812e7e5faa5c9c1886611a50c1be5f8b00515d7ef0c5ea`. Restauro real numa baseQA isolada `l1_joint_restore_20261024:55441`, container sem rede,174tabelas/contagens/checksums correspondentes; sem escrita na base activa. Originais privados e configuração ficaram no backup protegido `/var/backups/investmoz-l1-joint-delivery-20261024/backup`, sem expor valores de segredos. Mantém-se o patch de contenção compatível já ensaiado em QA: suspender edição/cálculo do painel L1, preservando leitores/resultados/relatórios/API/restantes formulários, pelo mesmo processo Git. O patch não está aplicado à release normal; não se executou reversão nem restauro da base activa.
**Instalador e delta:** activação concluída em24/10/2026 às12:30:02UTC; zero migrations/importações. Controlos automáticos normais do pipeline passaram (contrato isolado1teste/3106asserções;59unitários/317asserções;247frontend;compilação). Estes controlos pertencem ao processo de entrega existente; não foram reabertas rondas manuais dos casos aceites. Conferência posterior limitada ao delta instalado: revisão490527b9, hash do manifesto, quatro assets com11marcadores dos grupos/leitores/precisão, entrada actual do simuladorHTTP200 e disponibilidade fiscal existente. Não se criaram dossiers, pedidos ou cálculos sintéticos no activo.
**Preservação:** os seis ficheiros funcionais instalados correspondem aos hashes da candidata. Os369ficheiros persistentes/configuração correspondem ao backup. Comparadas as174tabelas por snapshot e leitura PostgreSQL com a mesma identidade/contexto antes e depois: diferenças apenas em `cache` e `sessions`; as restantes172tabelas, incluindo regras fiscais, drafts/resultados e dados de negócio, mantêm contagens/checksums. Não se declarou perda por visibilidade RLS nem se restauraram essas tabelas operacionais. Permanecem29fichas publicadas,28relações e408geometrias, sem reimportação.
**HTTP403 delimitado, sem mudar acesso:** duas leituras do mesmo URL `/build/revision.json`: cliente urllib padrão→403, `Server:cloudflare`, corpo `error code:1010`; cabeçalhos já usados no processo de entrega (`Mozilla/5.0 InvesteMoz-Delivery`)→200/revisão correcta. A [documentação oficial Cloudflare1010](https://developers.cloudflare.com/support/troubleshooting/http-status-codes/cloudflare-1xxx-errors/error-1010/) identifica recusa pela assinatura do cliente. A evidência delimita a camada e a diferença de cliente; não se afirma inspecção da regra específica no painel Cloudflare. Não há evidência de falha de permissão da aplicação; não foram alterados Cloudflare, Nginx, autenticação, RLS ou âmbito público.
**Saldo:** os12ramos demonstrados passam a entregues na homologação: P003–P007, P009, P011–P012, P017–P018 e P029–P030, com prefixo `AUD-SIM-L1-`. São15tratamentos dos grupos, para12ramos; o motor inclui também os dez tratamentos iniciais já aceites,25tratamentos condicionados no conjunto. Permanecem **120ramos funcionais pendentes +P001documental**:133IDs=12entregues+120funcionais+1documental. P001 não bloqueia tratamentos independentes; os restantes lotes continuam no plano. S1–S3, aquisições08–10/totais condicionados14–18, estabilização276d833a e fechos anteriores preservados. Estatísticas/IA continuam adiadas.
Evidência privada/agregada em `outputs/l1-joint-delivery-20261024/`; manifesto actualizado em [l1-joint-delivery-20261024.json](../simulations/l1-joint-delivery-20261024.json); recibo acrescentado no registo integrado existente, sem alterar os recibos históricos. O commit documental do recibo é posterior à revisão efectivamente instalada, conforme o processo já utilizado.
## 24/10/2026 — G2B social em QA e entrega conjunta L1 preparada
**G2B executável/demonstrado em QA, candidata conjunta `4a35d63c`; entrega preparada, não instalada. L1 continua aberto.** Preservados G1 (`5030116e`/`89ab2883`), G2A (`675cf38f`/`0e644b55`), precisão racional (`689e7cbe`) e provas aceites. Não se repetiram os ensaios de G1/G2A nem se regeneraram os Word/PDF das fracções. O conjunto concluído não depende do fecho dos restantes ramos ou da correspondência de P001.
### G2B — três ramos, seis tratamentos por tipo de entidade
IDs com prefixo `AUD-SIM-L1-`:
| Ramo | Tratamento público | Tratamento sem finalidade lucrativa |
| --- | --- | --- |
| P009 — assistência social/bens estreitamente conexos | `social_assistance_public` | `social_assistance_nonprofit` |
| P011 — actividade habitual de equipamentos sociais e bens/serviços conexos | `social_care_public` | `social_care_nonprofit` |
| P012 — uso de instalações para prática artística/desportiva/recreativa/educação física | `activity_facilities_public` | `activity_facilities_nonprofit` |
**Fonte/período:** CIVA republicado Lei13/2016, art.9(2)(a),(c),(d), PDF9/p.1426(267); art.19(1), PDF15/p.1426(273). Lei22/2022 art.1, art.10(1–2), PDF2/p.2252(26): definição de entidade pública e quatro condições cumulativas do organismo sem finalidade lucrativa. Reutilizado o cotejo das alterações2017/2020/2022/2025 já documentado; não houve nova validação de credibilidade da fonte. Âmbito técnico2026/MZN, operações internas; campo07 condicionado, sem dedução automática de compras ou apuramento integral.
Entidade pública: órgão/instituição da Administração Pública com funções administrativas do Estado nos termos legais. Organismo sem finalidade lucrativa: cumulativamente, não disponibiliza lucros nem interesse dos gerentes nos resultados; escrituração de todas as actividades disponível aos serviços fiscais; preços homologados ou, quando não sujeitos a homologação, inferiores aos comerciais análogos; ausência de concorrência directa com sujeitos passivos. Os controlos de uma entidade pública não exigem as quatro condições próprias do outro tipo. É a definição do artigo10, não um novo aprovador ou requisito organizacional criado pelo Codex.
Acrescem as condições da operação: assistência social e bens estreitamente conexos com fins/objecto reconhecidos; equipamento social enumerado, pertencente à entidade, na actividade habitual, com reconhecimento quando exigido; instalação explorada pela própria entidade e serviço a quem pratica a actividade, sem isenção automática de bilhetes para espectadores, rendas ou revenda. A escolha não decorre apenas do sector ou da designação «associação». A falta de uma condição/fundamento produz indeterminação; não zero nem isenção presumida. Os dados específicos são pedidos nos controlos já existentes, reutilizando contexto, moeda original, período, preço/representação e documentação válida do dossier.
**Refinamento documental P010:** o texto de9(2)(b), na mesma página, diz «ligadas à segurança», prestadas por entidades públicas. A rotulagem anterior «segurança social» foi corrigida na matriz; não se presume que a alínea seja um regime de segurança social. P010 continua pendente, com o mesmo ID e sem cálculo novo. Este refinamento não condiciona as alíneas(a)/(c)/(d).
Provas novas: dois testes frontend para seis tratamentos, todas as condições ausentes individualmente, fundamento ausente, preço líquido/incluído, contexto e relatório; um teste API privada/15asserções com guarda `simulator_fiscal_delta_20261021_test:55441` e rollback. Componente real em QA18629: condições públicas e não lucrativas distintas; retirada a condição de preços do organismo não lucrativo→indeterminação; reposta→seis linhas de100MZN/campo07=600MZN/IVA0, oportunidade311/Manica/2026-03, revisão3. Interface→API→recuperação idêntica→relatório HTML existente. Os livros aceites309/G2A e310/G1 permanecem iguais e legíveis, sem recálculo. Tipos/compilação conferidos apenas para o delta.
**Saldo exacto:**133IDs=**12ramos demonstrados em QA +120funcionais pendentes +P001documental**. Ramos cobertos: P003–P007, P009, P011–P012, P017–P018, P029–P030. Pendentes funcionais: P002, P008, P010, P013–P016, P019–P028, P031–P133. G2 mantém32ramos:8cobertos/24pendentes. G1/G2A/G2B somam15tratamentos para12ramos; com os dez tratamentos iniciais já aceites fora deste saldo, o conjunto oferece25tratamentos determinados sob condições, mais duas opções explicitamente indeterminadas. Não se confundem ramos, tratamentos, grupos ou pendência documental.
### Entrega conjunta — cobertura e dependências concretas
Manifesto executável/reconciliável: [l1-joint-delivery-20261024.json](../simulations/l1-joint-delivery-20261024.json). Inclui o motor L1 inicial, precisão racional e leitores/relatórios necessários, G1, G2A e G2B. Não é possível transportar só as opções novas sem as dependências L1 que ainda não estão na referência remota. Nenhum outro imposto, regime não demonstrado, tratamento de P001, alteração de fontes publicadas, migração, seeder, importação ou mudança de permissões integra a entrega. As fontes/cálculos publicados no servidor não são activados por este pacote.
A referência `origin/main` foi actualizada por `git fetch origin`, terminando em `1655f57e698fef4099c2886069a79cd1c920f792`. O `main` local é anterior e não foi usado como base de reposição. A diferença funcional desde o destino Git limita-se a seis ficheiros JS/TS: painel mensal, componente de operações, fracções, aquisições, motor de operações e consolidação do dossier. Mantêm-se S1–S3, aquisições08–10/totais condicionados14–18, estabilização276d833a, complementos, âncoras/mapas e alterações concorrentes herdadas. A pré-visualização `git merge-tree --write-tree` passou: sem conflitos, árvore igual à candidata e fast-forward possível. O bundle `outputs/iva-l1-g2-social-20261024/l1-joint-4a35d63c.bundle` passou `git bundle verify` (166937bytes), com hash no manifesto; contém a candidata funcional e história desde a base, sendo o recibo final um commit documental posterior. Qualquer avanço remoto antes da publicação exige nova reconciliação do delta, nunca force-push ou substituição do trabalho concorrente.
**Activação preparada:** confirmar revisão servida e backup/restauro válidos do destino no processo existente; actualizar novamente `origin/main`; integrar a candidata/conjunto por fast-forward quando possível ou merge reconciliado; build normal por `npm ci`/`npm run build` e webhook existente. Excluir outputs, basesQA, dados sintéticos, `public/hot`, segredos e dependências locais do artefacto activo. Não há migration/seed/regra fiscal de servidor a executar nem novo sinalizador obrigatório. O painel passa a disponibilizar as opções compiladas no percurso já existente; a condição de cada operação continua explícita. Após instalação conferir só revisão servida/manifesto/asset do simulador e o delta de visibilidade/contexto/leitura, reutilizando os ensaios aceites. Não criar pedidos sintéticos no activo.
A leitura prévia de `https://teste-investidor.tempo.co.mz/build/revision.json` nesta passagem devolveu **HTTP403**. Portanto, não se afirma uma nova conferência da revisão efectivamente servida, de backup activo ou do instalador protegido. A referência1655f57e é a referência Git remota e a última entrega comunicada. O controlo autenticado já existente permite conferir o destino no momento da instalação; não é dependência fiscal nem bloqueio de P001. Nesta passagem não houve push para main, instalação ou escrita no activo.
**Reversão preparada e ensaiada em QA:** [patch de consulta L1](../simulations/l1-joint-readonly-rollback-20261024.patch), verificado por `git apply --check` e pelo componente real em QA. Mantém catálogo, leitores, snapshots/histórico, relatórios, API e restantes formulários; suspende apenas edição/cálculo no painel L1 através de um fieldset desactivado com explicação visível. A consulta dos detalhes e o resultado de600MZN continuam disponíveis. É uma contenção operacional da interface, não uma alteração das permissões da API nem uma regra de negócio. Não está aplicada à candidata normal.
Em incidente: criar correcção Git sobre a revisão instalada, aplicar o patch sem ignorar conflitos, compilar/disponibilizar pelo mesmo processo e verificar apenas suspensão L1+consulta/relatório+demais percursos preservados. Corrigida a causa, reverter apenas esse commit de contenção e disponibilizar novamente. Não restaurar a base nem apagar snapshots para uma reversão de código. Voltar integralmente à revisão1655f57e ocultaria os tratamentos/leitores L1 e não é a reversão preferencial; se exigido por emergência, identificar essa redução e conservar base/originais/histórico para recuperação compatível. O bundle Git preparado transporta a história do conjunto desde a base remota; o recibo fixa candidata, hashes e resultado da pré-visualização. Ainda não é entrega na homologação.
Estatísticas/IA e os restantes lotes permanecem no plano acordado. Não se reabre o concluído para exigir uma nova ronda genérica de aceitação.
## 24/10/2026 — reconciliação dos IDs L1 e incremento G2A de saúde
**Incremento executável em QA, candidata `675cf38f`; L1 aberto; homologação inalterada.** Preservados `5030116e`/registo `89ab2883`, G1 e a apresentação das fracções Word/PDF, aceites pela Tempo com base no relatório. Não se repetiram esses ensaios nem se regeneraram os respectivos ficheiros. As entregas podem reunir conjuntos coerentes/utilizáveis; o fecho de todos os ramos não é condição técnica ou organizacional para entregar um conjunto. Esta passagem prepara QA, sem realizar integração/disponibilização.
### Reconciliação: ramo, tratamento e pendência documental
Os **133 IDs** do inventário original incluem **132 ramos funcionais (`P002`–`P133`) e uma pendência documental (`P001`/D06)**. Todos os IDs desta secção têm prefixo `AUD-SIM-L1-`. Um ramo representa uma cobertura legal/operacional e pode conter vários produtos/variantes. Um tratamento é uma opção executável no motor. Um grupo agrega ramos por regras/dados comuns; agrupar não encerra os ramos nem cria uma aprovação por ID. Não houve eliminação, fusão ou renumeração de IDs.
| Momento | Ramos cobertos em QA | Ramos funcionais pendentes | Pendências documentais | Total de IDs |
| --- | ---: | ---: | ---: | ---: |
| Inventário inicial do saldo, excluindo os dez tratamentos anteriores já aceites | 0 | 132 | 1 | 133 |
| Após G1, candidata5030116e | 4 | 128 | 1 | 133 |
| Após este incremento G2A | 9 | 123 | 1 | 133 |
Portanto, **133−4=129IDs pendentes após G1:128funcionais+P001**. O resumo anterior «128restantes» referia apenas os funcionais e precisava desta distinção explícita. P001 não foi encerrado para produzir a diferença. Os nove ramos cobertos correspondem, nestes dois incrementos, a nove tratamentos; essa correspondência não é presumida para o restante inventário.
| ID | Grupo/incremento | Tratamento executável | Estado/referência |
| --- | --- | --- | --- |
| P017 | G1 | `financial_stamp` | Aceite em QA5030116e; preservado |
| P018 | G1 | `insurance_stamp` | Aceite em QA5030116e; preservado |
| P029 | G1 | `public_waste` | Aceite em QA5030116e; preservado |
| P030 | G1 | `public_funeral` | Aceite em QA5030116e; preservado |
| P003 | G2/G2A | `health_assistive` | Executável/demonstrado em QA neste incremento |
| P004 | G2/G2A | `human_materials` | Executável/demonstrado em QA neste incremento |
| P005 | G2/G2A | `patient_transport` | Executável/demonstrado em QA neste incremento |
| P006 | G2/G2A | `mosquito_nets` | Executável/demonstrado em QA neste incremento |
| P007 | G2/G2A | `medicines` | Executável/demonstrado em QA neste incremento |
| P001 | D06 | Nenhum: correspondência05/06 | Pendente: instrução da edição recebida ou correspondência administrativa expressa aplicável |
Os restantes **123IDs funcionais** são exactamente **P002, P008–P016, P019–P028 e P031–P133** (1+9+10+103). Permanecem na mesma matriz com grupo e dependência próprios; o recibo desta passagem enumera o conjunto exacto e verifica a partição disjunta dos133IDs. G2 mantém32ramos:5cobertos por G2A e27pendentes. Os onze grupos originais mantêm-se; G2A é um subincremento de G2, não um novo grupo nem o fecho integral de G2.
### G2A — fonte, condições e reutilização
**Fonte executada:** CIVA republicado pela Lei13/2016, PDF9, p.1426(267), art.9(1)(b–f); direito à dedução confrontado com19(1), PDF15, p.1426(273). A rectificação de08/06/2017, BR89 PDF1, altera6/14 e anexos, não estas alíneas. Lei22/2022 art.1, PDF1, p.2252(25), mantém9(1)(b–f) por remissão. Lei10/2025 art.1, BR248 PDF12–13, altera outros artigos e19(2), conservando19(1); as isenções destas cinco operações não são convertidas em taxa5% nem em dedução automática de compras. O artigo19(1)(b)(v) não enumera9(1)(b–f).
Conferência adicional de âmbito: [Lei16/2020, art.1, BR246 suplemento, PDF20, p.2414(20), exemplar oficial UFSA](https://www.ufsa.gov.mz/query/Baixar_documento.php?nome=LEI+DO+SISTAFAE+-+LEI+14%2F2020+DE+23+DE+DEZEMBRO) altera9(13), não9(1)(b–f). O nome do download menciona SISTAFE, mas o exemplar completo contém as Leis13–16/2020; identificada a página da Lei16, sem inferir conteúdo do nome. A fonte parlamentar `https://www.parlamento.mz/wp-content/uploads/2025/05/iva.pdf` foi localizada:8páginas sem texto extraível; não usada como lei aprovada nem como regra G2A. Continua por cotejar para a operação temporal correspondente, sem bloquear G2A.
| Tratamento | Informação específica necessária nos controlos existentes |
| --- | --- |
| Compensação da deficiência/próteses/braille | Bem na enumeração e destino conforme categoria: deficiência, membro/órgão, fractura, visão ou audição; não qualquer equipamento médico |
| Órgãos/sangue/leite | Origem humana e natureza da transmissão; não origem animal ou serviço autónomo |
| Transporte de doentes/feridos | Doente/ferido, ambulância ou veículo apropriado e organismo devidamente autorizado; não basta destino hospitalar |
| Redes mosquiteiras | Identificação como rede mosquiteira; não extensão automática a outros bens preventivos |
| Medicamentos/artigos elegíveis | Produto farmacêutico exclusivamente terapêutico/profiláctico ou artigo enumerado da alínea com finalidade higiénica/medicinal/cirúrgica; não qualquer produto vendido por farmácia |
Condição comum: operação interna localizada em Moçambique, distinguindo importação/estrangeiro. Âmbito técnico2026/MZN. Reutilizados oportunidade, moeda original, localização, estabelecimento, período, valor/representação e fundamento já guardados. Acrescentam-se apenas a escolha do tratamento e a confirmação das condições específicas em falta, nos controlos existentes. Não há novos formulários, campos persistidos, schema ou permissões. Uma confirmação/documento pressuposto em QA não é certificação da operação real. A falta de natureza, destino, autorização ou fundamento gera indeterminação; não presume zero nem isenção pelo sector.
P008 não integra G2A: matérias-primas/componentes de medicamentos exigem integração do estabelecimento no Serviço Nacional de Saúde e correspondência com a posição aplicável do AnexoIII, incluindo rectificação2017/alteração2022. Não se acrescenta uma aprovação ou inscrição autónoma à condição legal. Essa verificação afecta P008, não as cinco alíneas independentes. Os outros27ramos de G2 conservam as dependências já registadas. O próximo subincremento pode usar perfis sociais/art.10 ou produtos/anexos após o cotejo específico, sem criar um bloqueio comum a G2.
### Prova dirigida e limites
Dois testes frontend novos cobrem os cinco tratamentos, cada condição ausente, fundamento ausente, preço líquido/IVA incluído isento e relatório/contexto recuperados. Um teste da API privada,13asserções, com guarda nominal `simulator_fiscal_delta_20261021_test:55441` e rollback. No componente real em QA18628, retirada a autorização do transporte: indeterminação; reposta: cinco operações de100MZN, campo07=500MZN, IVA0; snapshot da interface → gravação/API → recuperação idêntica → relatório HTML do exportador existente. O contexto de tratamento é309/Manica/2026-02/MZN; o livro G1 da oportunidade310 mantém-se exactamente igual ao snapshot aceite, sem novo cálculo/teste de G1.
O motor, gravação, recuperação e exportador foram reutilizados. As provas Word/PDF das fracções permanecem aceites e intactas; não são apresentadas como uma nova prova específica dos cinco textos G2A. Apenas o relatório/contexto novo foi conferido neste incremento. Não se declara apuramento integral, dedução automática, submissão à AT, fecho integral de G2/L1 ou entrega na homologação. Estatísticas e IA permanecem adiadas.
## 24/10/2026 — L1: incrementos por regras/dados comuns e primeiro grupo executável
**Continuação exclusivamente em QA, candidata `5030116e`, sobre `689e7cbe`/registo `8719e916`. L1 aberto; homologação inalterada.** Os 133 ramos foram distribuídos, sem duplicação, por onze incrementos funcionais e uma dependência documental. Não são 133 desenvolvimentos nem 133 aprovações. A correspondência individual está na mesma matriz, coluna «Grupo de execução», linhas `AUD-SIM-L1-P001`–`P133`; não foi criada outra matriz/backlog.
| Grupo | Ramos | Regra/dados comuns e próximo incremento |
| --- | ---: | --- |
| G1 — serviços isentos | 4 | Executável em QA: natureza do serviço, entidade, sujeição a Selo quando exigida, fundamento, preço e período. |
| G2 — outras isenções internas | 32 | Classificação de bens/serviços e entidades, anexos, afectação e renúncia apenas quando aplicáveis. Próximo núcleo de classificação. |
| G3 — benefícios temporários | 7 | Calendário, início/termo e alteração expressa. Texto integral Lei3/2025 para açúcar/óleo/sabões; solar com termo31/12/2025, sem presumir prorrogação. |
| G4 — exportações/assimiladas | 19 | Destino, transportador/adquirente, embarcação/aeronave e documentos; perfis partilhados de exportação, transporte e intermediação. |
| G5 — regimes especiais/ofertas | 7 | Regime, beneficiário, localização e finalidade; condições específicas do benefício, sem isenção inferida do mapa. |
| G6 — isenções na importação | 15 | Documento aduaneiro, código, origem, regime e destino. Classificação L1; dedução/documento L2 e direitos L13 conservam âmbito próprio. |
| G7 — bases especiais | 18 | Motor comum de composição: preço/margem/custo, descontos, encargos, comissão, moeda e datas; fórmula por alínea. |
| G8 — territorialidade material | 7 | Lugar dos bens/execução/imóvel, partes/estabelecimento e trajecto; consolidar alterações apenas nas cláusulas afectadas. |
| G9 — transfronteiriços/digitais | 11 | Natureza, residência/estabelecimento, destinatário/utilização e responsável pela liquidação; consolidar Lei10/2025 por operação. |
| G10 — assimiladas/especiais | 9 | Perfis de comissão, stocks, uso próprio, gratuitidade e transmissão de negócio; histórico de dedução quando necessário. |
| G11 — exigibilidade/período | 3 | Calendário comum de entrega, execução, factura, pagamento e adiantamento; mês orçamental não determina sozinho o período fiscal. |
| D06 — correspondência do formulário | 1 | Falta a instrução da edição recebida que distinga05/06 ou correspondência administrativa expressa aplicável. Não bloqueia grupos independentes. |
Sequência proposta após G1: G2 e perfis documentados de G4; G8/G11 para localização e período; G7/G10; G5/G6/G9 conforme a dependência concreta. G3 avança nas operações cuja regra temporal esteja determinada. D06 mantém trilho documental próprio. Cada incremento percorre dados → condições → cálculo/não aplicação/indeterminação → gravação → recuperação → relatório. As linhas conservam as dependências específicas; a tabela não as substitui. A estimativa544–912h continua preliminar, a rever por reaproveitamento medido; não se multiplica por133. SISA/IPRA continuam inventariados, com esforço separado.
### G1 — quatro tratamentos, um incremento comum
Linhas `P017`, `P018`, `P029`, `P030`: operações bancárias/financeiras sujeitas a Selo; seguro/resseguro/mediação conexa sujeitos a Selo; remoção de lixo por entidade pública ou por ela contratada; serviços funerários/cremação e bens acessórios efectuados por entidade pública. Fonte: **Lei22/2022, art.1, CIVA art.9(4),9(6),9(12)(b)/(c), PDF2, p.2252(26)**; natureza de entidade pública definida em10(1), na mesma página; direito à dedução confrontado com CIVA19(1), republicação2016 PDF15, p.1426(273). Aplicação técnica delimitada a2026/MZN, sem alterar taxa/regra fiscal nem activar regra publicada no servidor.
Reutilizados oportunidade, moeda original, estabelecimento, período, preço/representação e documento/pressuposto. Os únicos dados específicos são a escolha do tratamento e a confirmação fundamentada das suas condições, nos controlos já existentes. A sujeição a Selo é condição nas duas operações que a exigem; o cálculo de Selo não integra G1 nem é dependência técnica deste enquadramento. Não se presume isenção de toda a entidade, nem de funerárias privadas ou qualquer serviço de limpeza. Dados insuficientes produzem indeterminação, não zero. Campo07 apenas nas operações enquadradas; sem dedução automática de compras.
Provas novas: dois testes frontend; um teste API/11asserções, transacção revertida em `simulator_fiscal_delta_20261021_test:55441`. No componente real em QA, retirada a condição de Selo: operação/totais indeterminados; reposta: quatro linhas de100MZN, campo07=400MZN e IVA0. O resultado de interface foi guardado/recuperado pela API privada, sem diferença no snapshot, e usado nos exportadores reais. Os dez tratamentos aceites não foram reensaiados.
Preservação: a chave de actualidade passa a depender das regras efectivamente usadas, aceitando também a chave do catálogo anterior. Adicionar regras independentes não invalida o snapshot racional aceite. Teste dirigido confirma que permanece actual e byte a byte inalterado, sem recálculo; alterações das entradas/contexto continuam a invalidar a correspondência.
### Formatos finais — conferência e limite
Conferência dos DOCX produzidos pelo exportador existente e dos PDFs nativos do percurso de impressão, usando o snapshot racional já aceite e o novo resultado G1 recuperado. A suspeita inicial de corte/sobreposição foi afastada pela renderização ampliada: os originais estão legíveis. A alteração experimental de paginação foi retirada, sem mudança nos exportadores. Não se declara uma regressão nem uma correcção inexistente.
Os resultados exactos `20/21`, `1/21`, total base `10102/105` e IVA `5051/1050`, os decimais de apresentação `0.047619`/`4.810476`, MZN, oportunidade Manica, período, fonte e versão permanecem nos ficheiros finais. Prova concluída: DOCX efectivamente renderizados em LibreOffice25.8.3.1 (seis páginas racional, duas G1), com todas as páginas inspeccionadas; PDF nativo Edge (sete páginas racional, três G1). Não se afirma renderização pelo Microsoft Word: a automação local desse programa não terminou; o renderizador alternativo abriu os DOCX reais. O auxiliar bundled falhou inicialmente por falta de pdf2image; usaram-se conversão LibreOffice e renderização PyMuPDF. O registo identifica ficheiros e hashes.
**Saldo:** G1 com quatro ramos executáveis em QA;128ramos funcionais nos outros dez grupos e uma pendência D06. Agrupamento não equivale a implementação dos restantes. Não se declara L1 concluído nem disponibilizado na homologação. Fechos anteriores preservados; IRPC/IRPS e restantes lotes mantêm o plano acordado.
## 24/10/2026 — delta L1: precisão racional, cotejo do campo06 e inventário por operação
**Incremento em QA, candidata `689e7cbe`, sem integração/homologação.** Preservados candidata `544068e8`, registo `d89912e5` e as provas dos dez tratamentos. A Tempo apreciou o relatório comunicado; não lhe é atribuída conferência do manual ou da demonstração. L1 continua aberto. As referências abaixo actualizam os limites correntes, conservando o histórico anterior.
### IVA incluído a5% — pendência de cálculo resolvida, apresentação distinta da declaração
O bloqueio de um decimal periódico era uma escolha técnica inadequada, não uma restrição fiscal da Tempo. Retirado. Para preço brutoG, conserva-se exactamente base=G×20/21 e IVA=G/21, isto é, a identidade aritmética decorrente de contraprestação+5%IVA (CIVA arts.15(10)/17-A, Lei22/2022 art.1/2, PDF3–4, pp.2252(27)–(28)). Não se altera a taxa nem se cria um novo regime. O art.34 na redacção Lei10/2025 (PDF14, p.2180(14)) contém o divisor116: não é usado como fórmula para a taxa5%.
- **Cálculo/persistência:** fracções reduzidas com numerador/denominador inteiros em unidadesMZN, sem ponto flutuante. Exemplo1MZN→base20/21 e IVA1/21. Guardam-se nas linhas e nos totais por período. As parcelas condicionadas14–18 transportam as fracções, sem somar aproximações das linhas.
- **Apresentação:** seis casas, metade para cima, identificadas como apresentação técnica. Mostram-se também as fracções exactas. Exemplo1/21→0.047619; não se perde o resto no valor calculado.21linhas de1MZN produzem IVA total1MZN, não0.999999. A resolução decimal não é precisão limite do cálculo.
- **Regra declarativa:** não determinada por escolher seis casas de apresentação. Esta implementação não afirma como a AT arredonda o campo declarativo a5%, nem bloqueia por isso a simulação. Cálculo, recuperação e relatório estão disponíveis; preenchimento/submissão oficial requerem a regra específica, no recorte declarativo próprio.
- **Preservação:** nova versão `iva-operations-l1/2`; resultados/entradas v1 permanecem guardados e legíveis. Não há recálculo automático dos dez tratamentos aceites. Outra oportunidade conserva a sua versão original, sem alteração. Origem alterada/incompleta não conserva indevidamente uma fracção apresentada como resultado actual.
### Campo06 — cotejo concluído no acervo acessível; correspondência executável ainda pendente
1. **Exemplares recebidos:** DOCX SHA62c198e9/63d45fa8 e `VAT Return.pdf` SHA9c0b99a8, p.1/quadro8:05 «Vendas isentas com direito a dedução»;06 remete para18(1)(b);07 isentas sem direito;08–10 dedução de compras. As instruções/rodapés existentes nos exemplares não discriminam os conjuntos05/06. Originais inalterados; não se repetiu a prova dos restantes campos.
2. **Lei:** CIVA republicado2016 PDF14, p.1426(272),18(1)(b)=imposto devido na importação. PDF15, p.1426(273),19(1)(b)(i–v)=operações que permitem o exercício do direito, com múltiplos ramos; operações no estrangeiro são apenas o subponto(ii). Lei10/2025 PDF13 mantém a estrutura de19(1) e altera19(2). Não se deduz daqui uma regra «todo o estrangeiro vai para06», nem se coloca IVA de importação numa base de vendas.
3. **Formulário antigo da AT:** [e-declaração, Modelo A](https://edeclaracao.at.gov.mz/formularios/iva/view/formulario_iva_ma.aspx), conteúdo oficial indexado consultado24/10:03 contém a remissão18(1)(b),04 isentas sem direito,05 imobilizado,06 existências,07 outros bens/serviços; taxa17%/remissão16. Portanto, a instrução antiga para «campo06» não corresponde ao campo06 da edição recebida. A consulta directa da página terminou em timeout; a leitura é identificada como conteúdo indexado, não sessão preenchida/submetida.
4. **Publicação localizada:** BR72 de15/04/2019, despacho/art.1(a), identificado no índice primário com aprovação do Modelo A e exemplar a17%. O ficheiro integral em `archive.gazettes.africa` respondeu **HTTP403**; não se declara leitura integral nem se usa o índice como instrução inexistente de05/06 da edição recebida. Não se contornou a restrição.
**Conclusão delimitada:** os números pertencem a layouts distintos e a remissão recebida não coincide com o texto legal consultado. A associação do antigo03 ao actual06 por simples deslocamento numérico continua insuficiente, porque existe também05 na edição recebida. Falta a instrução dessa edição que distinga os conjuntos05/06 ou uma correspondência administrativa expressa aplicável. Essa é a dependência de `AUD-SIM-L1-P001`. **Não se declara campo06 resolvido/activado.** A interface explica o cotejo; a localização da operação estrangeira é analisada independentemente desse número. O cálculo das linhas já delimitadas continua disponível; totais que dependem de uma classificação não determinada permanecem indeterminados.
Fontes locais verificáveis: [CIVA2016](../simulations/source-archive/Lei-13-2016-CIVA-BR-156-10-suplemento.pdf), [Lei22/2022](../simulations/source-archive/Lei-22-2022-IVA-republicada-BR-250-2-suplemento.pdf), [Lei10/2025 no BR248](../simulations/source-archive/BR-248-I-Suplemento-1-2025.pdf). A fonte oficial é aceite; a pendência é a correspondência concreta/edição, não nova validação da sua credibilidade nem aprovação jurídica genérica.
### Restante cobertura — operações individualizadas na mesma matriz
A linha-mãeAUD-SIM-INT02 conserva os fechos e passa a remeter para **133 linhas de cobertura `AUD-SIM-L1-P001`–`AUD-SIM-L1-P133` na matriz CSV existente**, sem outro backlog. São ramos legais/operacionais do saldo anteriormente agregado, não133 novos impostos ou lotes. Cada linha identifica operação, artigo/fonte, dependência exacta, dados/condições em falta, próximo incremento e critério de percurso completo; nenhum desses ramos é declarado implementado por este inventário.
Sequência dos incrementos do saldo: **R1** incidência/territorialidade e momento; **R2** isenções internas por natureza/entidade e listas; **R2T** limites temporais/cessação; **R3E** exportações/assimiladas; **R3Z** regimes/benefícios especiais; **R4** bases e composição da contraprestação. Os serviços transfronteiriços/digitais e operações assimiladas constam individualmente emR1D/R1S, não numa linha «outros». **R3I** delimita isenções na importação, conservando os cálculos/documentos/dedução deL2 e os direitos deL13 nos lotes próprios. A dependência06 éD06; o arredondamento declarativo está separado do cálculo racional disponível e do futuro fecho declarativoL3.
As linhas discriminam as alíneas ainda não implementadas dos arts.9,12,13,14,15 e as operações/localizações específicas dos arts.3/4/6/7/8. Produtos abrangidos por anexos exigem posição identificada; açúcar/óleo/sabões e solar têm dependência temporal concreta, sem prorrogação presumida para2026. Fontes antigas com regimes/benefícios especiais têm a alteração aplicável indicada como dependência antes de activar esse ramo. SISA/IPRA permanecemL15/L16 por analisar, esforço separado; não foram implementados nem somados à estimativa preliminar.
### Provas deste delta e estado da entrega
**Seis testes frontend novos aprovados**, exclusivamente no delta: fracções periódicas, total antes da apresentação, gravação/recuperação e preservação do snapshot aceite da primeira oportunidade, totais14–18/relatório, informação insuficiente/origem alterada e não associação automática ao campo06. Não executada novamente a suite dos dez tratamentos aceites.
**API real QA:1teste/10asserções**, base nominal isolada `simulator_fiscal_delta_20261021_test:55441`, guarda de identidade e transacções revertidas. Preenchimento/calculadora real com três preços periódicos1/100/0.02MZN, na segunda oportunidade310→gravação privada→recuperação exacta das fracções/resultados e snapshot309 original→relatório HTML/CSV sem recálculo. Interface distingue fracções e decimais, incluindo total04=5051/1050MZN, apresentado4.810476. Prova visual dirigida apenas ao delta. Sem novo PDF exigido ou regeneração dos PDFs aceites.
TypeScript/build conferidos; código frontend e representação racional reutilizam a API/rascunho e exportador existentes. Nenhum motor/parametrização fiscal publicado, alteração de schema/permissões/dados activos ou entrega na homologação. O recibo integrado `iva_l1_followup_qa_20261024` regista a candidata, artefactos e dependência documental06 ainda aberta. As544–912h continuam preliminares; este refinamento não é medição de esforço nem orçamento adicional aprovado.
## 24/10/2026 — L1 executável em QA: operações de IVA, cobertura e saldo explícitos
**Estado: incremento executado em QA, não entregue na homologação; não é fecho integral de L1 nem do IVA.** Base `e8c39a1b`, descendente da entrega `1655f57e`. Ramo `codex/iva-l1-20261024`; candidata executavel `544068e8`. Preservados os fechos S1–S3, aquisições08–10/totais14–18 e referência `276d833a`. Mantém-se o plano único abaixo; o inventário admite omissões e recebe SISA/IPRA para análise antes do futuro Imobiliário. Nenhum imposto/regra publicado ou activado no destino, schema/configuração/permissões/dados activos alterados.
### Operações efectivamente executáveis
Período técnico deste incremento: Janeiro–Dezembro2026, MZN explícito; não é declaração de expiração legal das fontes. Cada linha mostra artigo, fonte, condições e fundamento/pressuposto; a escolha do sector ou da localização não selecciona automaticamente isenção/taxa.
| Operação incluída | Tratamento/campo | Fonte exacta e condições recolhidas |
| --- | --- | --- |
| Operação interna geral | 16%;01/02; preço líquido ou IVA incluído | CIVA arts.1,6,15(1),17(1–2); Lei22/2022 art.1, PDF3, p.2252(27); Lei10/2025 art.1, art.34, PDF14, p.2180(14). Confirmar localização tributável/regime normal e contraprestação completa sem base especial/isenção/taxa reduzida. |
| Serviços médicos/sanitários privados e estreitamente conexos | 5%;03/04 | CIVA arts.15(10),17-A(a), Lei22/2022 arts.1/2, PDF3–4, pp.2252(27)–(28). Prestador hospitalar privado/clínica/dispensário/similar e natureza do serviço confirmados. |
| Ensino privado reconhecido e operações conexas pelo estabelecimento | 5%;03/04 | CIVA arts.15(10),17-A(b), mesma fonte. Integração no Sistema Nacional de Ensino e reconhecimento ministerial; não aplicar a toda venda autónoma de material escolar. |
| Formação profissional privada e fornecimentos conexos | 5%;03/04 | CIVA arts.15(10),17-A(c), mesma fonte. Confirmar formação/conexão e prestador privado. |
| Lições ministradas a título pessoal | 5%;03/04 | CIVA arts.15(10),17-A(d), mesma fonte. Confirmar título pessoal e matérias do ensino escolar/superior. |
| Exportação de bens pelo vendedor/terceiro por sua conta | Isenção com direito nas condições legais;05 | CIVA art.13(1)(a), republicação2016, PDF12, p.1426(270); art.19(1)(b)(i), PDF15, p.1426(273). Saída efectiva documentada ou pressuposto expresso da projecção; cliente estrangeiro por si só não basta. Não é dedução automática de compras nem classificação genérica «taxa zero». |
| Locação de imóvel para habitação | Isenção sem direito;07 | CIVA art.9(5), Lei22/2022 art.1, PDF2, p.2252(26); condições do art.19(1), republicação2016 PDF15. Não aplicar a hospedagem nem locação comercial/industrial. |
| Serviços médicos/sanitários públicos e estreitamente conexos | Isenção sem direito;07 | CIVA arts.9(1)(a),10(1), Lei22/2022 art.1, PDF1–2, pp.2252(25)–(26), conjugados com art.19(1). Confirmar serviço e natureza pública do prestador. |
| Ensino público reconhecido e operações conexas pelo estabelecimento | Isenção sem direito;07 | CIVA arts.9(3)(a),10(1), mesma fonte. Confirmar integração/reconhecimento, natureza pública e operação efectivamente conexa. |
| Formação profissional pública e fornecimentos conexos | Isenção sem direito;07 | CIVA arts.9(3)(b),10(1), mesma fonte. Confirmar formação/conexão e prestador público. |
Originais legais: [CIVA republicado2016](../simulations/source-archive/Lei-13-2016-CIVA-BR-156-10-suplemento.pdf), [Lei22/2022](../simulations/source-archive/Lei-22-2022-IVA-republicada-BR-250-2-suplemento.pdf), [BR248, Leis7–12/2025](../simulations/source-archive/BR-248-I-Suplemento-1-2025.pdf). As páginas PDF são contadas a partir de1; as páginas impressas estão indicadas separadamente. A leitura identificou que o art.17-A está na **página PDF4**, não3; a referência do novo catálogo foi corrigida antes do registo da candidata. Fontes aceites, sem nova validação genérica de credibilidade.
**Preço incluído:** geral — art.34 na redacção Lei10/2025: base=preço×100/116, arredondada à unidade mais próxima; IVA=preço−base. Escolha técnica explícita no empate: metade para cima. Exemplo QA1161MZN→base1001/IVA160/bruto1161. Reduzido — inversão aritmética dos arts.15(10)/17-A quando exacta até6casas:105MZN→base100/IVA5/bruto105. Não aplicar o divisor116 à taxa5%. Quando a inversão a5% exige arredondamento não delimitado, a linha permanece indeterminada; pode ser usada uma base líquida documentada, sem a inventar. Valores líquidos usam precisão técnica6casas/metade para cima, declarada no relatório, sem afirmar arredondamento final da AT.
### Dados reutilizados e únicos complementos pedidos
- Reutilizados: oportunidade em tratamento, moeda original, localização e rótulos, vendas e meses do plano/tesouraria, representação do preço quando já declarada e fonte do pressuposto. Reutilização não apaga edições deliberadas nem converte moeda.
- Informação adicional por operação, só quando em falta: descrição discriminante, estabelecimento (não inferido do local do projecto), tratamento, condições específicas e documento/pressuposto. Período, valor e preço líquido/incluído são herdados; só são preenchidos quando inexistentes ou divergentes. Nos valores reutilizados, a fonte existente preenche o pressuposto, evitando reescrita.
- Repartição explícita de vendas agregadas conserva total/período/moeda/representação da origem; parcelas devem reconciliar com esse total. Divergência, origem alterada, período fora de cobertura, preço desconhecido ou ausência de condição não gera zero. Uma lista vazia só constitui ausência após confirmação de cobertura do mês.
- Mantidos o rascunho privado, API, idempotência, permissões, relatório/CSV e o armazenamento já existentes: extensão `salesOperations` por oportunidade dentro de `modelo_a_snapshot`, sem novo formulário obrigatório nem schema. Cada recálculo guarda a versão anterior. A execução mensal geral existente continua conservada como execução separada e não é somada novamente às linhas L1.
### Divergências delimitadas e saldo de L1
1. **Campo06:** o exemplar remete para art.18(1)(b), que na republicação2016 PDF14 é imposto devido na importação, enquanto o direito relativo a operações no estrangeiro surge no art.19(1)(b)(ii), PDF15. Não se troca silenciosamente a remissão nem se atribui esse campo a qualquer operação estrangeira. Entrada própria conserva os dados como indeterminados. A pendência não impede calcular as outras linhas; totais que possam depender dela continuam indeterminados.
2. **Preço incluído5% com inversão não exacta:** falta delimitar o método de arredondamento dessa situação, não a existência da taxa ou o cálculo de uma base líquida conhecida. Apenas esse ramo permanece sem resultado.
3. **Outras coberturas ainda por implementar em L1:** restantes isenções dos arts.9/12–14, bases especiais do art.15(2), territorialidade/autoliquidação/serviços digitais e outras operações não enumeradas nesta tabela; não presumir cobertura por seleccionar «Outra operação». O registo da venda é conservado, com informação insuficiente e próximo passo. As remissões temporárias do art.9(13)/(16) não são prorrogadas automaticamente. Importações/deduções parciais/regularizações mantêm os lotes próprios do plano.
4. **Compras afectas a operações reduzidas/isentas/outras:** não reutilizar automaticamente a dedução exclusiva geral do lote08–10 em contexto novo misto. CIVA art.19(2), Lei10/2025 art.1, PDF13, p.2180(13), menciona as operações17-A. Neste incremento, essas parcelas exigem afectação específica em L2; resultados antigos são conservados, não convertidos em dedução universal. Quando a classificação L1 está actual, os seus campos01–07 alimentam o apuramento parcial existente; classificação alterada/incompleta não é substituída pela antiga execução geral nem por declarações de ausência.
### Referências exactas das incompatibilidades legislativas referidas no levantamento
- **IRPC:** Lei12/2025, arts.3/4/6, BR248 suplemento1 **PDF20, p.2180(20)**: revogação dos arts.39,47,75(2),76; transição dos regimes simplificados para contabilidade organizada; entrada01/01/2026. Daqui decorre a incompatibilidade da recomendação corrente dos coeficientes art.47 em2026. Não se presume prazo de transição adicional nem se elimina a possibilidade de regra histórica do respectivo período.
- **IRPS:** Lei11/2025, arts.3/4/6, mesmo BR **PDF18, p.2180(18)**: revogações discriminadas e transição dos sujeitos da segunda categoria dos regimes simplificados; entrada01/01/2026. Não é regra de cálculo mensal derivada dos escalões anuais.
- **IVA:** Lei10/2025, arts.2/3/5, mesmo BR **PDF15, p.2180(15)**: art.53 transitório, revogação das SubsecçõesI/II da SecçãoIV e do art.21(8), entrada01/01/2026. São estes trechos que impedem escolher automaticamente antigos limiares de isenção/simplificado do resumo. Não demonstram a extinção das isenções por operação do art.9, que continuam a ser analisadas separadamente.
- **Taxa geral e reduzida:** Lei22/2022 arts.1/2/6, **PDF3–4, pp.2252(27)–(28)**, arts.17(1)/17-A e início01/01/2023. As condições operacionais não são substituídas pelo exemplo de uma empresa mista do documento descritivo.
### Provas dirigidas e limitações da demonstração
**15 testes frontend aprovados:** sete novos L1 e oito da ligação às aquisições directamente alterada; sem repetir suites S1–S3, mapas, projectos âncora ou outros fechos sem delta. Abrangem cada tratamento enumerado, preço incluído geral e reduzido exacto, preço desconhecido, condições em falta, zero explícito, divergência de moeda/período/origem, repartição, histórico, segunda oportunidade sem alterar a primeira, ligação aos totais e recusa de dedução universal.
**API real:1 teste/13 asserções aprovado**, executado sobre payload calculado na interface, base nominal isolada `simulator_fiscal_delta_20261021_test:55441`, identidade conferida e transacções revertidas. Gravação→mesmoID na repetição idempotente→recuperação exacta; outra conta recusada; fingerprint das regras inalterado. Não é teste de motor servidor L1: os valores projectados são calculados pelo componente frontend e conservados no rascunho privado existente, como no recorte de aquisições; nenhuma nova regra publicada/admitida no consumidor fiscal do servidor.
**Interface:** componente real num ensaio sintético isolado, com12 entradas (dez tratamentos e dois casos indeterminados). Retirar fundamento da linha geral→calcular→IVA indeterminado; repor fundamento→calcular→IVA160; linha privada105 com IVA incluído→base100/IVA5. Guardar estado da interface→API real QA→recuperação→relatório HTML/CSV, sem recálculo na exportação, com montantes, oportunidade309/Manica, estabelecimento, Janeiro2026, artigos/fontes e revisão preservados. Histórico da versão insuficiente permanece guardado. Não se confunde o armazenamento local do ecrã de ensaio com a prova API.
HTML/CSV novos em `outputs/iva-l1-qa-20261024/`, com modelo e payloads sintéticos. Relatório visualmente conferido no browser. Não foi produzido/conferido novo PDF nativo nesta passagem, nem se regeneraram os PDFs aceites. TypeScript e build do incremento conferidos; ficheiros de prova, limitações e estado exacto no recibo integrado `iva_l1_qa_20261024`.
**Esforço:** houve reaproveitamento efectivo do simulador, rascunho/API, permissões e exportador. Não se declara um novo motor ou implementação do orçamento completo40–64h; esse intervalo era preliminar para o âmbito mais amplo de L1. Não existe medição fiável de horas desta passagem. Reestimar o saldo por cobertura remanescente, sem transformar544–912h em prazo contratado nem acrescentar SISA/IPRA ao total sem análise.
## 24/10/2026 — inventário fiscal reconciliado e proposta de fecho antes do Imobiliário
**Estado: análise concluída; lotes abaixo propostos, não implementados nem activados nesta passagem.** Decisão expressa da Tempo: concluir o inventário fiscal acordado antes do Imobiliário. Estatísticas e IA continuam posteriores. A tabela abaixo é a referência única do plano, dentro do manual e da matriz existentes (AUD-SIM-INT02); não constitui uma segunda matriz. As secções anteriores que colocavam todo o IRPC/IRPS em desenvolvimento posterior ao Imobiliário ficam ultrapassadas por esta sequência, mantendo os respectivos factos históricos e fechos.
### O que foi efectivamente conferido
Inventário das pastas autorizadas `Modelos` e `portal do investidor/Notas`, distinguindo originais de temporários de edição; leitura integral do texto de `Impostos e seu tratamento.docx`, do documento IRPC, dos escalões IRPS, da estrutura/células/fórmulas dos dois XLSX e das três páginas do M/22 PDF. O cotejo Modelo A/VAT Return já aceite foi reutilizado. Inventário, hashes e extracções privadas em `outputs/fiscal-inventory-plan-20261024/`; sem importar, publicar, alterar ou recalcular os originais. Os dados preenchidos de terceiros nos exemplares Excel não são pressupostos do investidor e não são reproduzidos neste manual.
- `Impostos e seu tratamento.docx`: SHA256 `aade59537c6c19eb56b017cfc317407d5c6ae758c8a15a390508cd5f6e9ae3ed`. O documento intitulado `O IRPC (Imposto sobre o Rendimento das Pessoas Coletivas) .docx` tem bytes/hash distintos (`4a4668c116c1abae26a334d55e3c9fe508c1ab31aa2a19fe7b88e374c4b34d16`), mas texto extraído integralmente igual: não são duas confirmações independentes.
- `Escalões e Taxas do IRPS.docx`: SHA256 `ea9fdfaf8a8682645e0e3dad3e2e165520783e9f72e8d23acdf684d8359d690e`; contém escalões anuais, não uma tabela mensal de retenção por trabalhador.
- `IRPC_IRPS.xlsx`: guias M/39 IRPC e M/19 IRPS, sem fórmulas. A remissão CIRPS na guia intitulada IRPC exige correspondência própria; um formulário não demonstra um motor de cálculo.
- `Cópia de IRPS-IMOP.xlsx`: sete folhas, uma oculta; 102 fórmulas, 15 referências directas `#REF!` e 18 erros em valores guardados. Inclui M/22, M/20 e anexos. Não serve de oráculo numérico sem reparar e cotejar cada correspondência; os erros não equivalem a zero.
- M/22 PDF versus XLSX: quadro de cálculo 12 versus 10; campo 302 «colecta» e 309 «IRPC liquidado» no PDF aparecem com designações trocadas no XLSX; referências PEC art.71 versus art.66 e base da colecta também diferem. A edição/período e a semântica de cada campo precedem a ligação executável. O modelo antigo não prova que um regime continua disponível em 2026.
Código reconciliado na revisão corrente, descendente da entrega `1655f57e`: consumidor público de regra fiscal limitado ao IVA demonstrado; `custom_planning` é planeamento por pressupostos do utilizador. Cadastro de fontes, parâmetros, expressões, governação e campos de entrada IRPC/IRPS/ISPC/INSS/retenções/aduaneiro-ICE são reutilizáveis, mas não equivalem a consumidores fiscais completos. Não se declara ausência de regras no cadastro protegido a partir desta leitura de código. Não houve novas consultas protegidas, ensaios de aceitação ou alterações ao destino.
### Divergências concretas e efeito delimitado
1. **IRPC simplificado:** o texto recomenda coeficientes 20%/30% do art.47 como solução corrente. A Lei12/2025 revoga esse artigo e determina transição para contabilidade organizada, com entrada em vigor em 01/01/2026. Não implementar esses coeficientes como opção corrente de 2026. Eventual cálculo histórico exige a regra do respectivo período; não se inventa prazo de transição. A Lei11/2025 também altera o enquadramento simplificado da segunda categoria IRPS.
2. **IVA:** o documento alterna limiares de 250.000 e 750.000 e recomenda isenção automática. A Lei10/2025 altera/revoga os antigos regimes e contém transição. Esses números não seleccionam automaticamente o regime em 2026. Operação a 5%, isenção com/sem direito à dedução e taxa zero são situações distintas; saúde/ensino não tornam todos os materiais ou vendas da entidade sujeitos a 5%.
3. **ISPC:** o resumo 3%/4% é incompleto perante a Lei9/2025: há diferentes actividades, escalões, exclusões, opções e tratamentos, incluindo taxas de 5%, 12%, 15% e excesso de volume. Não transformar a descrição em taxa única nem presumir tributação marginal. A elegibilidade é anterior à comparação ISPC/IRPC/IVA.
4. **Retenções e créditos:** nem todo serviço implica retenção uniforme de 20%; natureza do rendimento, residência, sujeito que retém, período, excepções e eventual convenção determinam o cálculo. Distinguir IRPC autónomo de mais-valias do tratamento de despesas não documentadas. Guia M/39, crédito estimado, crédito declarado, reconhecimento, compensação e dinheiro recebido são estados diferentes. Afirmações genéricas sobre original físico carimbado, caducidade de cinco anos, compensação automática ou cruzamento bancário da AT não se tornam restrições do portal sem disposição/procedimento concretamente aplicável.
5. **Selo e encargos:** não foi demonstrada incidência universal de 0,4% sobre toda factura acrescida de IVA. Falta cotejar verba, base, isenções, interacção com IVA e período na tabela aplicável. Exemplos de juros «MIMO + margem», coimas e multas não fornecem parâmetros suficientes. A pendência afecta esses cálculos, não bloqueia os restantes impostos.
6. **Trabalho:** escalões anuais IRPS não são convertidos numa tabela mensal dividindo por 12. As taxas INSS 4% empregador/3% trabalhador têm confirmação operacional oficial para TCO, mas base contributiva, exclusões, período e regime TCO/TCP precisam de correspondência própria. O exemplo de salário líquido que só desconta INSS não é líquido final quando há IRPS.
Fontes oficiais aceites reutilizadas: BR248, suplemento1 de29/12/2025 (Leis7–12/2025), CIVA2016/rectificação2017, republicação2022 e acervo fiscal existente. A origem oficial não requer nova aprovação jurídica genérica: aplicam-se as decisões Tempo de08/10 e22/10. Conferir edição, artigo, período, parâmetros e transições é execução da fonte aceite. Os antigos pedidos genéricos de «Fiscalista» constantes de registos históricos não criam um aprovador adicional para estes lotes.
### Tabela única — situação, lacunas, reutilização, dependências e esforço
Âmbito-base proposto: investimento e obrigações de 2026, com saldos/factos de anos anteriores necessários ao apuramento e respectiva proveniência. A possibilidade técnica de escolher outro ano não demonstra cobertura histórica: recalcular integralmente todos os impostos de2000–2025 ou activar anos futuros não integra implicitamente esta estimativa. Cada lote explicita os períodos que consegue calcular.
As estimativas são **horas de trabalho técnico**, não calendário nem medição já executada. Incluem cotejo específico restante, implementação, persistência/recuperação, relatório, provas dirigidas em QA e preparação/conferência do delta Git. Assumem reutilização da infra-estrutura existente; não incluem espera pela entrega de documentos efectivamente em falta, decisão organizacional nova ou integração externa da AT. Não se contabiliza novamente o IVA fechado. A sequência L1–L14 é proposta; dependências abaixo são concretas, não uma obrigação de bloquear todos os lotes em série.
| Lote / âmbito | Já disponível | Falta implementar de ponta a ponta | Dados reutilizáveis | Dependências concretas | Esforço estimado |
| --- | --- | --- | --- | --- | --- |
| Encerrado — planeamento, S1–S3 e IVA entregue | Planeamento financeiro; moeda original; recuperação por ID; IVA mensal geral líquido MZN/2026; aquisições08–10 no âmbito demonstrado e totais14–18 condicionados | Nenhum saldo nos lotes aceites. Não extrapolar para IVA integral | Vendas, compras, orçamento, custos, tesouraria, dossier por oportunidade, gravação e exportações | Preservar `cd10cccb`/`355656b5`, `1655f57e`/`e8c39a1b` e referência `276d833a` | 0 h de reimplementação |
| L1 — IVA: operações e enquadramento | Vendas mensais e operação interna geral líquida | Classificar operações03–07 e outras situações aplicáveis; IVA incluído, isenções/direito à dedução, taxa reduzida e períodos por linha/estabelecimento | Produtos, vendas, entidade, localização e oportunidade | CIVA e alterações2025; incidência por operação, sem inferir regime de sector/forma jurídica | 40–64 h |
| L2 — IVA: dedução restante e regularizações | Compras08–10, líquido/IVA/bruto, documento, mês e afectação explícita no recorte aceite | Importações documentadas, dedução parcial/pro-rata, exclusões, activos e regularizações12–13; condições por parcela | Compras, activos, fornecedor, documento, calendário, parcela não dedutível | L1 quando determina direito; artigos de dedução e regularização. Importação pode usar documento aduaneiro existente, sem esperar pelo cálculo de direitos de L13 | 48–72 h |
| L3 — IVA: saldo e fecho do Modelo A | Totais14–18 quando todas as parcelas estão determinadas | Campos19–27 aplicáveis, transporte de crédito, rectificações/substituição, imposto a entregar e pedido de reembolso; estados documentados e relação com tesouraria | Execuções mensais, parcelas L1/L2, documentos e histórico | L1/L2 para apuramento integral; saldo anterior identificado e procedimento de reembolso aplicável. Pedido não significa deferimento/recebimento | 48–80 h |
| L4 — ISPC: elegibilidade e apuramento | Campos de enquadramento e motor genérico | Regime/actividade/volume elegível, opções/exclusões, escalões/taxas aplicáveis, mínimo, trimestre e retenções específicas; comparação só entre regimes admissíveis | Vendas por actividade, custos, NUIT, natureza jurídica e residência confirmada | Lei9/2025 e regulamentação/período; distinguir receita anual, trimestre e excesso | 40–64 h |
| L5 — retenções IRPC e guias | Documentos de compras/vendas e guia M/39 como exemplar | Rendimento pago/recebido, entidade que retém, residente/não residente/EP, evento temporal, excepções e casos digitais/e-money; guia e comprovativo ligados à operação | Contrapartes, facturas, pagamentos, período, moeda, oportunidade | Artigos aplicáveis por rendimento, taxa/convenção se invocada; corrigir remissões do exemplar. Sem submissão electrónica AT presumida | 32–56 h |
| L6 — IRPC anual: regime normal | Projecções, resultados financeiros, activos e custos | Ponte resultado contabilístico→matéria colectável, custos admitidos/não admitidos, amortizações, perdas fiscais e colecta; correspondência M/20/M/22 | Plano financeiro e dados comuns; L5 para retenções sofridas | Código IRPC e Lei12/2025; exercício fiscal, residência/EP, saldos/perdas com ano e prova; resolver diferenças PDF/XLSX | 48–80 h |
| L7 — IRPC: tratamentos específicos | Contexto sectorial/territorial e fontes cadastradas | Casos identificados: tributação autónoma de despesas e mais-valias, taxas reduzidas/cooperativas, isenções/benefícios e operações especiais com condições rastreadas | Base L6, despesas, activos, relação territorial documentada | Disposição e período por tratamento; localização por si só não concede benefício. Lista de tratamentos aplicáveis explicitada no lote | 48–80 h |
| L8 — IRPC: liquidação e pagamentos | Parcelas L5–L7 e formulários originais | PPC/PEC, deduções/créditos, dupla tributação quando aplicável, saldo anual a pagar/recuperar; calendário e reconciliação com pagamentos | Retenções, colecta, saldos anteriores, pagamentos/comprovativos | Base anterior para PPC/PEC, regras de limitação/transporte; não usar cinco anos ou compensação automática sem cotejo específico | 32–56 h |
| L9 — INSS: contribuição social | Massa salarial agregada e referência TCO3%+4% | Trabalhadores, remuneração/base contributiva, exclusões, empregador/trabalhador, TCO/TCP, declaração e encargos por período | Equipa/custos, entidade e calendário; cadastro de pessoa partilhado com L10 | Regulamento e alterações aplicáveis, natureza do vínculo; confirmar base antes da taxa. Contribuição social, distinta dos impostos | 24–40 h |
| L10 — IRPS: retenção mensal e líquido | Entrada IRPS, custos salariais e escalões anuais documentais | Tabela mensal própria, remunerações/benefícios, situação pessoal relevante, retenção, salário líquido e contribuição ligada a L9 | Mesmo trabalhador/folha salarial, pagamentos e retenções | Tabela mensal do período, residência e condições familiares necessárias; não dividir escalões anuais por12 | 40–64 h |
| L11 — IRPS: apuramento anual e outras categorias | Escalões anuais e exemplares M/19/M/20/anexos | Categorias aplicáveis aos rendimentos documentados, actividade independente, capitais/prediais/mais-valias, englobamento/tratamento autónomo, deduções, retenções e saldo | Dados pessoais autorizados, rendimentos/despesas, activos e L10 | Código IRPS/Lei11/2025, âmbito/categoria/período e deduções pessoais; diferenças de edição dos anexos. Não copiar regras IRPC | 48–80 h |
| L12 — Imposto do Selo | Factos/contratos/documentos financeiros do projecto | Incidências por verba aplicável às operações documentadas, base, taxa/valor fixo, sujeito, isenção e obrigação; explicar não aplicação | Contratos, financiamento, garantias, recibos e contexto do negócio | Código/tabela e alterações da verba/período; resolver exemplo genérico0,4% sem o aplicar a todas as facturas | 24–48 h |
| L13 — direitos aduaneiros | Entrada aduaneira/ICE e fontes no inventário anterior | Classificação pautal, origem, valor aduaneiro, despesas de composição, câmbio explícito e direitos/benefícios aplicáveis | Equipamentos/compras, transporte, seguro, documentos de importação e localização | Pauta/Lei8/2025, código/origem e data. Não inferir código pela descrição livre nem autorização de benefício pela localização | 40–72 h |
| L14 — ICE | Componente de entrada e fonte Lei7/2025 inventariada | Produtos/operações abrangidos, tributação específica/ad valorem/mista quando prevista, base/unidade e período; ligação à importação ou produção interna | Quantidades, produtos, preços, origem e L13 quando importado | Código/tabelas ICE e alterações; ligação à base de IVA somente quando aplicável, sem bloquear produção interna por ausência de cálculo aduaneiro | 32–56 h |
| L15 — SISA e delimitação territorial | Operações/localização do investimento; ainda sem consumidor fiscal específico | Analisar transmissão onerosa/direitos sobre imóveis, âmbito autárquico ou nacional, incidência/isenções/base/período e percurso completo | Imóvel, preço/documento, partes e localização; não equiparar DUAT a propriedade da terra | Cotejo do Código Tributário Autárquico/legislação nacional e alterações; fonte municipal confirma família, não taxa nacional uniforme | Por estimar após análise; não incluído em544–912h |
| L16 — Imposto Predial Autárquico e encargos imobiliários aplicáveis | Localização e orçamento; sem cálculo específico demonstrado | Analisar posse/uso/valor patrimonial, município, exercício e isenções; discriminar taxas administrativas de impostos | Imóvel, uso, entidade e valores documentados | Base legal e tabela da autarquia/período; não converter preço de compra em valor patrimonial | Por estimar após análise; não incluído em544–912h |
**Total preliminar original L1–L14 (L15/L16 ainda por analisar): 544–912 h (68–114 dias-pessoa de8h).** Não é promessa de prazo nem autorização automática de publicação. Rever a estimativa ao fechar o cotejo específico de cada lote, identificando exactamente a alteração. Não há um lote invisível adicional para relatórios ou persistência: fazem parte de cada linha.
Origem do âmbito: IVA, IRPC, ISPC, Selo e INSS constam de `Impostos e seu tratamento`; IRPS consta do documento de escalões e dos modelos. Direitos aduaneiros e ICE já constam do inventário fiscal/entrada existente e do BR248: são mantidos explicitamente na proposta, não atribuídos ao novo texto. Retenções, PPC/PEC, créditos, juros e obrigações são tratamentos das famílias respectivas, não impostos novos. Juros/encargos só são calculados no lote respectivo com facto, base, vencimento e parâmetro legal determinados; caso contrário ficam expressamente não calculados. Reclamações judiciais, minutas avulsas, execução bancária e ligação técnica de submissão à AT não se presumem requisitos de cálculo por aparecerem numa conversa do documento. Outros tributos não identificados neste acervo não são acrescentados silenciosamente ao significado de «terminar».
**Revisão para o futuro Imobiliário:** o inventário anterior era incompleto: faltavam SISA e IPRA explicitados. Acrescentados L15/L16 como recortes por analisar, mantendo a mesma tabela. A informação municipal oficial identifica [SISA imobiliária](https://cmcpemba.gov.mz/impostos-e-taxas/iasisa) e [IPRA](https://cmcpemba.gov.mz/impostos-e-taxas); serve aqui para confirmar a omissão, não para activar taxas. Há inconsistências concretas no índice municipal (descrição de veículos repetida em SISA), pelo que não se transportam essas descrições nem taxas para uma regra nacional. A delimitação legal/autárquica e temporal permanece por cotejar no ramo respectivo. IVA de locação habitacional está em L1; vendas sujeitas a SISA (art.9(12)(d)) e arrendamentos não habitacionais exigem enquadramento próprio; rendas/retenções e mais-valias permanecem L5–L8/L10–L11, contratos/financiamento em L12. Nenhuma destas ligações presume venda de terra ou transmissibilidade do DUAT. Outros encargos municipais efectivamente necessários serão inventariados com fonte e operação, sem acrescentar indiscriminadamente todos os tributos nacionais.
### Critério comum de fecho, aplicado separadamente a cada imposto/lote
1. **Dados:** entrada geral ou ficha herda a oportunidade em tratamento; reutiliza informação válida de perfil/organização/dossier/fontes, conservando autoria e edições. Projecto/localização não substitui residência fiscal. Pergunta apenas o que falta/diverge; valores sem moeda/origem não são convertidos ou reinterpretados silenciosamente.
2. **Enquadramento:** cada ramo tem condição, artigo/fonte, edição e período; distingue aplicável, não aplicável com explicação e informação insuficiente com próxima acção concreta. Cobertura não implementada é pendência do produto, nunca «não aplicável» ao investidor. Uma divergência documental suspende apenas o ramo dependente.
3. **Cálculo:** parcelas e total, arredondamento, limites, zero real e desconhecido são distintos. Caso normal e excepções delimitadas têm exemplos verificáveis, incluindo fronteiras de escalão/período. Crédito não é recebimento e compra não gera automaticamente dedução.
4. **Gravação/recuperação:** mesma execução, ID, oportunidade, pressupostos, parcelas, fonte e versão; permissões conservadas e recuperação independente da admissão de novos cálculos. Alteração posterior cria versão/explica divergência sem reescrever resultado aceite.
5. **Relatório:** ecrã, ficheiro final e recuperação apresentam os mesmos montantes/moeda/período/contexto/versão/fontes e pendências, incluindo paginação dos blocos novos. Dois investimentos: alterar apenas o segundo mantém o primeiro intacto. Provas anteriores sem delta são reutilizadas.
6. **Entrega:** demonstrar o lote em QA isolada, integrar pelo Git preservando alterações concorrentes, conferir revisão servida e apenas o delta/dependências. Sem activar cálculos por importar/publicar fontes e sem simular reconhecimento/submissão da AT. Operações em dados activos mantêm backup/restauro e acção autenticada aplicável.
O fecho integral exige cobertura executável e demonstrada das linhas acordadas, com operações/regimes/períodos discriminados. A ausência de dados de um investidor pode gerar resultado indeterminado correctamente explicado; a ausência de uma regra necessária ou de implementação permanece saldo do lote. As seis associações e os dois originais documentais anteriores não bloqueiam globalmente este plano: só afectam uma regra quando a dependência efectiva for identificada.
**Saldo histórico do levantamento anterior, actualizado pelo incremento L1 acima:** fontes e implementação reconciliadas, divergências e proposta finita registadas; zero alterações funcionais, zero impostos activados e zero novas provas exigidas à Tempo. S1–S3, aquisições08–10/totais14–18 e demais fechos permanecem encerrados. L1 tem agora incremento executável em QA com saldo discriminado acima; L2–L16 não estão implementados por esta passagem.
## 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,11 @@
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';
import { SimulationOperations } from './simulation-operations';
export function SimulationMonthlyIva({draft, busy, onCalculate, onChange}: {
draft: Record<string, unknown>; busy: boolean;
@@ -16,6 +18,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 +28,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 +39,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 +79,14 @@ 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>}
<SimulationOperations draft={draft} onChange={onChange} />
<SimulationAcquisitions draft={draft} onChange={onChange} />
<SimulationModeloA draft={draft} />
</section>;
}
@@ -0,0 +1,41 @@
import { useState } from 'react';
import { modeloA06Correspondence, newSalesOperation, operationTreatments, recordSales, reuseSales, salesBook, salesCurrent, salesReportRows, saveSalesBook, splitSales, type SalesBook, type SalesOperation } from '@/lib/iva-operations';
export function SimulationOperations({draft,onChange}:{draft:Record<string,unknown>;onChange:(field:string,value:string)=>void}) {
const [error,setError]=useState('');
let book:SalesBook;
try{book=salesBook(draft);}catch(cause){return <p role="alert">{String(cause)}</p>;}
const run=(action:()=>void)=>{try{action();setError('');}catch(cause){setError(cause instanceof Error?cause.message:String(cause));}};
const save=(next:SalesBook)=>onChange('modelo_a_snapshot',saveSalesBook(draft,next));
const change=(id:string,patch:Partial<SalesOperation>)=>run(()=>save({...book,rows:book.rows.map(r=>r.id===id?{...r,...patch}:r)}));
return <section aria-label="Operações IVA por oportunidade" className="space-y-3 rounded border p-3">
<h4 className="font-semibold">Operações de IVA — enquadramento das vendas</h4>
<p>Oportunidade {String(draft.context_opportunity||'entrada geral')}; moeda original {book.currency||'por indicar'}. Cobertura2026/MZN. Reutilize as vendas e complete apenas o enquadramento. Este bloco classifica as operações; não substitui nem soma novamente a execução mensal geral acima.</p>
<p>Uma empresa pode ter operações com tratamentos diferentes. Os valores são projecções sob pressupostos declarados, sem certificar facturas, reconhecer créditos ou submeter uma declaração à AT.</p>
<details><summary>Correspondência do campo06 e limites do formulário</summary><p>{modeloA06Correspondence}</p></details>
{error&&<p role="alert">{error}</p>}
<button type="button" className="underline" onClick={()=>run(()=>save(reuseSales(draft,book)))}>Reutilizar vendas e períodos existentes</button>
<button type="button" className="ml-3 underline" disabled={!book.periods.length} onClick={()=>run(()=>save({...book,rows:[...book.rows,newSalesOperation(crypto.randomUUID(),book.periods[0].period,book.currency)]}))}>Acrescentar operação em falta</button>
{book.rows.map(row=>{const rule=operationTreatments.find(r=>r.id===row.treatment);return <details className="rounded border p-3" key={row.id}><summary>{row.description||'Operação adicional'} — {row.period}</summary>
<p>Origem: {row.source}; {row.sourceAmount??'sem valor de origem'} {row.sourceCurrency}; {row.sourcePeriod}. O estabelecimento não é inferido da localização do projecto.</p>
<div className="grid gap-3 md:grid-cols-2">
<label>Descrição da operação<input className="block rounded border p-2" value={row.description} onChange={e=>change(row.id,{description:e.target.value})}/></label>
<label>Estabelecimento<input className="block rounded border p-2" value={row.establishment} onChange={e=>change(row.id,{establishment:e.target.value})}/></label>
<label>Período<input type="month" className="block rounded border p-2" min="2026-01" max="2026-12" value={row.period} onChange={e=>change(row.id,{period:e.target.value})}/></label>
<label>Valor declarado<input inputMode="decimal" className="block rounded border p-2" value={row.amount} onChange={e=>change(row.id,{amount:e.target.value})}/></label>
<label>Representação do preço<select className="block rounded border p-2" value={row.basis} onChange={e=>change(row.id,{basis:e.target.value as SalesOperation['basis']})}><option value="">Ainda desconhecida</option><option value="net">Sem IVA</option><option value="included">IVA incluído</option></select></label>
<label>Tratamento da operação<select className="block rounded border p-2" value={row.treatment} onChange={e=>change(row.id,{treatment:e.target.value,confirmed:[]})}>{operationTreatments.map(t=><option value={t.id} key={t.id}>{t.label}</option>)}</select></label>
</div>
<p>{rule?.article}; {rule?.source}. Aplicação nesta versão: Janeiro–Dezembro2026, sujeita às condições seguintes.</p>
{rule?.conditions.map((condition,index)=><label className="block" key={index}><input type="checkbox" checked={row.confirmed.includes(index)} onChange={e=>change(row.id,{confirmed:e.target.checked?[...row.confirmed,index]:row.confirmed.filter(i=>i!==index)})}/> {condition}</label>)}
<label className="block">Documento ou pressuposto que sustenta estas condições<textarea className="block w-full rounded border p-2" value={row.evidence} onChange={e=>change(row.id,{evidence:e.target.value})}/></label>
<p>Se precisar de repartir uma venda agregada, conserve o valor de origem e prepare linhas discriminadas. Não altere silenciosamente a sua representação monetária.</p>
<button type="button" className="mr-3 underline" onClick={()=>run(()=>save(splitSales(book,row.id,crypto.randomUUID())))}>Repartir explicitamente esta venda, conservando o total de origem</button>
<button type="button" className="underline" onClick={()=>run(()=>save({...book,rows:book.rows.filter(r=>r.id!==row.id),periods:book.periods.map(p=>p.period===row.period?{...p,complete:false}:p)}))}>Remover esta linha do preenchimento (resultado anterior conservado)</button>
</details>;})}
{book.periods.map(p=><label className="block" key={p.period}><input type="checkbox" checked={p.complete} onChange={e=>run(()=>save({...book,periods:book.periods.map(v=>v.period===p.period?{...v,complete:e.target.checked}:v)}))}/> {p.period}: incluí todas as operações deste período; lista vazia significa ausência expressamente confirmada.</label>)}
<p>Limites: outras isenções/bases especiais e correspondência do campo06 permanecem por delimitar. IVA incluído a5% conserva a fracção exacta e apresenta um decimal a6casas; a aproximação de apresentação não constitui regra declarativa. O desconhecido nunca é assumido como zero.</p>
<button type="button" disabled={!book.periods.length} className="rounded bg-emerald-800 p-2 text-white disabled:opacity-50" onClick={()=>run(()=>save(recordSales(draft,book)))}>Calcular e guardar operações IVA</button>
{book.result&&<><p role="status">{salesCurrent(draft,book)?'Resultado correspondente às entradas.':'Resultado histórico conservado; reveja as alterações antes de recalcular.'}</p><dl>{salesReportRows(draft).map(([,label,value,origin],index)=><div className="break-inside-avoid border-t py-2" key={index}><dt className="font-semibold">{label}</dt><dd>{value}<small className="block">{origin}</small></dd></div>)}</dl></>}
</section>;
}
+22
View File
@@ -0,0 +1,22 @@
/** Exact amounts in currency units; decimal formatting is presentation, never a tax rule. */
export type FiscalFraction = { numerator:string; denominator:string };
function parts(value:FiscalFraction):[bigint,bigint] {
if(!/^-?\d{1,80}$/.test(value.numerator)||!/^\d{1,80}$/.test(value.denominator)||BigInt(value.denominator)===0n)throw new Error('Representação racional inválida; sem substituição por zero.');
return [BigInt(value.numerator),BigInt(value.denominator)];
}
export function fraction(n:bigint,d=1n):FiscalFraction {
if(d===0n)throw new Error('Denominador nulo.');if(d<0n){n=-n;d=-d;}
let a=n<0n?-n:n,b=d;while(b){const r=a%b;a=b;b=r;}return {numerator:String(n/(a||1n)),denominator:String(d/(a||1n))};
}
export function decimalFraction(raw:string):FiscalFraction {
if(!/^\d{1,18}(\.\d{1,6})?$/.test(raw))throw new Error('Montante decimal inválido.');const [a,b='']=raw.split('.');return fraction(BigInt(a+b),10n**BigInt(b.length));
}
export function addFractions(values:FiscalFraction[]):FiscalFraction {return values.reduce((a,b)=>{const [an,ad]=parts(a),[bn,bd]=parts(b);return fraction(an*bd+bn*ad,ad*bd);},fraction(0n));}
export function subtractFractions(a:FiscalFraction,b:FiscalFraction):FiscalFraction {const [an,ad]=parts(a),[bn,bd]=parts(b);return fraction(an*bd-bn*ad,ad*bd);}
export function positiveFraction(value:FiscalFraction):FiscalFraction {return parts(value)[0]>0n?value:fraction(0n);}
export function fractionPresentation(value:FiscalFraction):{decimal:string;approximate:boolean} {
const [raw,d]=parts(value),n=raw<0n?-raw:raw,scaled=n*1000000n;
const q=(scaled+d/2n)/d,dec=(q%1000000n).toString().padStart(6,'0').replace(/0+$/,'').padEnd(2,'0');
return {decimal:`${raw<0n?'-':''}${q/1000000n}.${dec}`,approximate:scaled%d!==0n};
}
export function fractionLabel(value:FiscalFraction):string {parts(value);return `${value.numerator}/${value.denominator}`;}
+208
View File
@@ -0,0 +1,208 @@
import { modeloAPurchaseReferences } from './iva-modelo-a.ts';
import { monthlyIvaInput, monthlyIvaKey, readMonthlyIvaRecord, readMonthlyIvaSettings } from './monthly-iva.ts';
import { restoreBusinessPlanSnapshot } from './business-financial-plan.ts';
import { salesBook, salesCurrent as salesOperationsCurrent } from './iva-operations.ts';
import { addFractions, decimalFraction, fractionLabel, fractionPresentation, positiveFraction, subtractFractions, type FiscalFraction } from './fiscal-rational.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>; exactFields?:Record<string,FiscalFraction|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);
const operations=(readAcquisitionStore(draft.modelo_a_snapshot) as Store & {salesOperations?:Record<string,unknown>}).salesOperations?.[acquisitionScope(draft)];
return `${raw.length}:${hash.toString(16)}${operations?`:${JSON.stringify(operations)}`:''}`;
}
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;
const operations=salesBook(draft);
if(operations.rows.length || operations.result) {
const classified=salesOperationsCurrent(draft,operations)?operations.result?.periods.find(p=>p.period===period):null;
for(const field of ['01','02','03','04','05','06','07'])fields[field]=classified?.fields[field]??null;
if(!classified)reasons.push('Operações L1 alteradas ou sem resultado correspondente: recalcule a classificação; não usar01/02 antigos nem ausência presumida.');
else reasons.push(...classified.reasons);
}
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((operations.rows.length || operations.result) && operations.rows.some(r=>r.period===period && !['general','export_goods'].includes(r.treatment)) && matching.some(r=>rows.find(x=>x.id===r.id)!.deductible!=='0.00')) {
fields[field]=null;
reasons.push(`Campo${field}: operações reduzidas/isentas/outras exigem afectar cada aquisição; não reutilizar dedução exclusiva geral como se fosse universal (L2).`);
}
}
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);
const classified=salesOperationsCurrent(draft,operations)?operations.result?.periods.find(p=>p.period===period):null;
if(classified?.exactFields) {
const exactFields:Record<string,FiscalFraction|null>={};
for(const f of ['01','02','03','04','05','06','07','08','09','10','11','12','13'])exactFields[f]=classified.exactFields[f]??(fields[f]===null?null:decimalFraction(fields[f]!));
for(const [f,parts] of [['14',['01','03','05','06','07']],['15',['08','09','10','11','12']],['16',['02','04','13']]] as const)exactFields[f]=parts.some(k=>exactFields[k]===null)?null:addFractions(parts.map(k=>exactFields[k]!));
exactFields['17']=exactFields['16']&&exactFields['15']?positiveFraction(subtractFractions(exactFields['16'],exactFields['15'])):null;
exactFields['18']=exactFields['16']&&exactFields['15']?positiveFraction(subtractFractions(exactFields['15'],exactFields['16'])):null;
for(const f of Object.keys(exactFields))fields[f]=exactFields[f]?fractionPresentation(exactFields[f]!).decimal:null;
reasons.push('Totais derivados das fracções exactas, sem somar aproximações das linhas; decimais a6casas são apresentação técnica, não arredondamento declarativo.');
return {period,fields,exactFields,reasons};
}
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.exactFields)out.push(['IVA mensal',`Modelo A — ${period.period} — precisão exacta`,Object.entries(period.exactFields).map(([f,v])=>`${f}: ${v?fractionLabel(v):'Indeterminado'}`).join('; '),'Fracções em MZN; apresentação decimal técnica, sem regra declarativa presumida.']);
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}`],
];
+161
View File
@@ -0,0 +1,161 @@
import { acquisitionScope, readAcquisitionStore } from './iva-acquisitions.ts';
import { monthlySalesFromDraft, readMonthlyIvaSettings } from './monthly-iva.ts';
import { addFractions, decimalFraction, fraction, fractionLabel, fractionPresentation, type FiscalFraction } from './fiscal-rational.ts';
export const operationVersion = 'iva-operations-l1/2';
// The accepted v2 full-catalogue key predates scoped keys. Freeze its members
// so later, independent catalogue additions cannot invalidate those snapshots.
const legacyV2TreatmentIds = ['general','private_health','private_education','private_training','personal_lessons','export_goods','housing_rent','public_health','public_education','public_training','foreign_operation','other'];
const source2022 = 'Lei22/2022, BR250, 2.º suplemento, PDF1–4 (2252(25)–(28)); arts.1/2/6; efeitos01/01/2023';
const source2016 = 'CIVA republicado Lei13/2016, BR156, 10.º suplemento, PDF9–15 (1426(267)–(273))';
const source2025 = 'Lei10/2025, BR248, suplemento1, PDF14–15 (2180(14)–(15)); arts.1/2/3/5; efeitos01/01/2026';
const healthGoodsSource = 'CIVA republicado Lei13/2016, PDF9, p.1426(267), art.9(1)(b–f); PDF15, p.1426(273), art.19(1); Lei22/2022 art.1, PDF1, p.2252(25), mantém9(1)(b–f). Âmbito técnico2026/MZN';
const internalOperation = 'Operação interna localizada em Moçambique; não é importação nem operação localizada no estrangeiro';
export const modeloA06Correspondence = 'Exemplar recebido:06 é base de operações isentas, com remissão18(1)(b); nessa lei18(1)(b) trata IVA de importações, enquanto19(1)(b) enumera operações com direito. O mesmo exemplar já tem05 para vendas isentas com direito. O formulário antigo da AT usa03 para a remissão e06 para existências. Não há correspondência unívoca nem instrução aplicável que separe05/06 no acervo conferido: não atribuir06 automaticamente a operações no estrangeiro ou importar instruções antigas por número.';
export type OperationTreatment = {id:string; label:string; field:string|null; rate:number|null; article:string; source:string; conditions:string[]};
const nonprofitConditions = [
'Não distribui nem disponibiliza lucros; os corpos gerentes não têm interesse directo ou indirecto nos resultados, por si ou interposta pessoa',
'Escrituração abrange todas as actividades e está à disposição dos serviços fiscais para comprovação',
'Preços homologados pela autoridade competente ou, quando não susceptíveis de homologação, inferiores aos de operações análogas das empresas comerciais sujeitas a imposto',
'Não entra em concorrência directa com sujeitos passivos do imposto',
];
function socialTreatment(id:string,label:string,article:string,conditions:string[],nonprofit:boolean):OperationTreatment {
return {id,label,field:'07',rate:0,article:`CIVA arts.9(2)(${article}),10(${nonprofit?'2':'1'}),19(1)`,source:'CIVA republicado Lei13/2016, PDF9, p.1426(267), art.9(2)(a/c/d); PDF15, p.1426(273),19(1); Lei22/2022 art.1, PDF2, p.2252(26),10(1–2). Âmbito técnico2026/MZN',conditions:[internalOperation,...(nonprofit?nonprofitConditions:['Entidade pública: órgão ou instituição da Administração Pública que desempenha funções administrativas do Estado, nos termos da legislação aplicável']),...conditions]};
}
export const operationTreatments:OperationTreatment[] = [
{id:'general',label:'Operação interna geral',field:'01',rate:16,article:'CIVA arts.1,6,15(1),17(1–2); art.34 (Lei10/2025)',source:`${source2016}; ${source2022}; ${source2025}`,conditions:['Operação localizada e tributável em Moçambique, no regime normal','Contraprestação completa; sem base especial do art.15(2), isenção ou taxa reduzida']},
{id:'private_health',label:'Serviços médicos e sanitários privados',field:'03',rate:5,article:'CIVA arts.15(10),17-A(a)',source:source2022,conditions:['Serviço médico/sanitário ou operação estreitamente conexa','Prestador hospitalar privado, clínica, dispensário ou similar']},
{id:'private_education',label:'Ensino privado reconhecido',field:'03',rate:5,article:'CIVA arts.15(10),17-A(b)',source:source2022,conditions:['Ensino ou bens/serviços conexos prestados pelo estabelecimento','Estabelecimento privado integrado no Sistema Nacional de Ensino e reconhecido pelo Ministério competente']},
{id:'private_training',label:'Formação profissional privada',field:'03',rate:5,article:'CIVA arts.15(10),17-A(c)',source:source2022,conditions:['Formação profissional ou fornecimentos conexos pelo prestador, não venda autónoma de materiais','Prestador privado']},
{id:'personal_lessons',label:'Lições ministradas a título pessoal',field:'03',rate:5,article:'CIVA arts.15(10),17-A(d)',source:source2022,conditions:['Lições ministradas a título pessoal','Matérias de ensino escolar ou superior']},
{id:'export_goods',label:'Exportação de bens pelo vendedor',field:'05',rate:0,article:'CIVA arts.13(1)(a),19(1)(b)(i)',source:source2016,conditions:['Bens expedidos/transportados para o estrangeiro pelo vendedor ou terceiro por sua conta','Saída documentada, ou pressuposto expresso da projecção; não mera venda a cliente estrangeiro']},
{id:'housing_rent',label:'Locação de imóvel para habitação',field:'07',rate:0,article:'CIVA art.9(5), redacção Lei22/2022; art.19(1)',source:source2022,conditions:['Locação de imóvel para fins de habitação','Não é hospedagem nem locação comercial/industrial ou serviço autónomo']},
{id:'public_health',label:'Serviços médicos e sanitários públicos',field:'07',rate:0,article:'CIVA arts.9(1)(a),10(1),19(1)',source:source2022,conditions:['Serviço médico/sanitário ou operação estreitamente conexa','Estabelecimento hospitalar público, dispensário ou similar; natureza pública confirmada']},
{id:'public_education',label:'Ensino público reconhecido',field:'07',rate:0,article:'CIVA arts.9(3)(a),10(1),19(1)',source:source2022,conditions:['Ensino ou fornecimentos conexos pelo estabelecimento','Estabelecimento público integrado no Sistema Nacional de Ensino e reconhecido pelo Ministério competente']},
{id:'public_training',label:'Formação profissional pública',field:'07',rate:0,article:'CIVA arts.9(3)(b),10(1),19(1)',source:source2022,conditions:['Formação profissional ou fornecimentos conexos pelo prestador','Prestador público']},
{id:'financial_stamp',label:'Operação bancária/financeira sujeita a Selo',field:'07',rate:0,article:'CIVA art.9(4), redacção Lei22/2022; art.19(1)',source:`${source2022}; ${source2016}`,conditions:['A operação é bancária ou financeira, não um serviço acessório autónomo','Sujeição a Imposto do Selo identificada para esta operação; não depende de calcular o Selo neste bloco']},
{id:'insurance_stamp',label:'Seguro/resseguro ou mediação conexa sujeitos a Selo',field:'07',rate:0,article:'CIVA art.9(6), redacção Lei22/2022; art.19(1)',source:`${source2022}; ${source2016}`,conditions:['Seguro/resseguro ou serviço conexo efectuado por corrector ou outro mediador de seguros','Sujeição a Imposto do Selo identificada para esta operação']},
{id:'public_waste',label:'Remoção de lixo pública ou por entidade contratada',field:'07',rate:0,article:'CIVA art.9(12)(b), redacção Lei22/2022; art.19(1)',source:`${source2022}; ${source2016}`,conditions:['Prestação de serviço de remoção de lixo, não qualquer serviço de limpeza','Efectuada por entidade pública ou por entidade por ela contratada, com relação identificada']},
{id:'public_funeral',label:'Serviços funerários/cremação públicos e bens acessórios',field:'07',rate:0,article:'CIVA art.9(12)(c), redacção Lei22/2022; art.19(1)',source:`${source2022}; ${source2016}`,conditions:['Serviço funerário/cremação ou transmissão de bem acessório a esse serviço','Efectuado por entidade pública; não aplicar automaticamente a funerária privada']},
{id:'health_assistive',label:'Bens de compensação da deficiência, próteses e braille',field:'07',rate:0,article:'CIVA arts.9(1)(b),19(1)',source:healthGoodsSource,conditions:[internalOperation,'Bem identificado na enumeração: cadeira de rodas/veículo semelhante, aparelho/máquina/impressora braille, prótese ou material de compensação','Destino documentado conforme a categoria: deficiência, substituição de membro/órgão, fractura, pessoa invisual ou correcção da audição; não qualquer equipamento médico']},
{id:'human_materials',label:'Órgãos, sangue e leite humanos',field:'07',rate:0,article:'CIVA arts.9(1)(c),19(1)',source:healthGoodsSource,conditions:[internalOperation,'Transmissão de órgãos, sangue ou leite de origem humana, identificada; não produto de origem animal nem serviço autónomo']},
{id:'patient_transport',label:'Transporte de doentes/feridos por organismo autorizado',field:'07',rate:0,article:'CIVA arts.9(1)(d),19(1)',source:healthGoodsSource,conditions:[internalOperation,'Transporte de doente ou ferido em ambulância ou outro veículo apropriado','Organismo que efectua o transporte devidamente autorizado, com autorização identificada; não basta o destino hospitalar']},
{id:'mosquito_nets',label:'Transmissão de redes mosquiteiras',field:'07',rate:0,article:'CIVA arts.9(1)(e),19(1)',source:healthGoodsSource,conditions:[internalOperation,'Bem identificado como rede mosquiteira; não extensão automática a outros produtos de prevenção']},
{id:'medicines',label:'Medicamentos e artigos terapêuticos/profilácticos elegíveis',field:'07',rate:0,article:'CIVA arts.9(1)(f),19(1)',source:healthGoodsSource,conditions:[internalOperation,'Medicamento/especialidade farmacêutica ou produto farmacêutico exclusivamente terapêutico/profiláctico; ou pasta, gaze, algodão hidrófilo, tira/penso adesivo ou análogo','Finalidade elegível identificada: terapêutica/profiláctica nos produtos farmacêuticos; higiénica, medicinal ou cirúrgica nos artigos enumerados, mesmo impregnados/revestidos; não qualquer produto vendido por farmácia']},
...([false,true] as const).flatMap(nonprofit=>{
const suffix=nonprofit?'nonprofit':'public',entity=nonprofit?'organismo sem finalidade lucrativa':'entidade pública';
return [
socialTreatment(`social_assistance_${suffix}`,`Assistência social — ${entity}`,'a',['Prestação de assistência social ou transmissão de bens estreitamente conexa, identificada','Fins e objecto da entidade reconhecidos pelas autoridades competentes, com fundamento identificado'],nonprofit),
socialTreatment(`social_care_${suffix}`,`Equipamentos sociais/creches/lares — ${entity}`,'c',['Prestação ou bem estreitamente conexo no exercício da actividade habitual de creche, jardim de infância, centro de tempos livres, lar ou outro equipamento social enumerado no art.9(2)(c)','Equipamento pertencente à entidade e fins/objecto reconhecidos pelas autoridades competentes; não qualquer alojamento ou empresa de cuidados'],nonprofit),
socialTreatment(`activity_facilities_${suffix}`,`Prática artística/desportiva/recreativa — ${entity}`,'d',['Serviço efectuado pela própria entidade que explora o estabelecimento ou instalação','Instalação destinada à prática artística, desportiva, recreativa ou de educação física e serviço prestado a quem a pratica; não isenção automática de bilhetes para espectadores'],nonprofit),
];
}),
{id:'foreign_operation',label:'Operação no estrangeiro — analisar territorialidade',field:null,rate:null,article:'CIVA art.6: localização por natureza da operação; art.19(1)(b)(ii) não determina sozinho a caixa06 do formulário',source:source2016,conditions:[]},
{id:'other',label:'Outra operação / enquadramento ainda desconhecido',field:null,rate:null,article:'Fundamento específico ainda necessário; não presumir taxa ou isenção',source:'Sem fonte operacional determinada para esta linha',conditions:[]},
];
export type SalesOperation = {id:string;period:string;description:string;establishment:string;amount:string;basis:''|'net'|'included';treatment:string;confirmed:number[];evidence:string;sourceAmount:string|null;sourcePeriod:string;sourceCurrency:string;source:string; splitSource?:{id:string;amount:string|null;period:string;currency:string;basis:string}};
export type SalesBook = {currency:string;rows:SalesOperation[];periods:Array<{period:string;complete:boolean}>; result?:SalesResult; history?:SalesResult[]};
export type SalesLineResult = {id:string;field:string|null;net:string|null;vat:string|null;gross:string|null;rate:number|null;status:'calculated'|'exempt'|'indeterminate';reason:string;article:string;source:string;exact?:{net:FiscalFraction;vat:FiscalFraction;gross:FiscalFraction}};
export type SalesResult = {version:string;revision:number;calculatedAt:string;opportunity:string;location:string;currency:string;key:string;inputs:SalesOperation[];rows:SalesLineResult[];periods:Array<{period:string;fields:Record<string,string|null>;exactFields?:Record<string,FiscalFraction|null>;reasons:string[]}>;rounding:string};
type Store = ReturnType<typeof readAcquisitionStore> & {salesOperations?:Record<string,SalesBook>};
export function newSalesOperation(id:string,period:string,currency:string):SalesOperation {return {id,period,description:'',establishment:'',amount:'',basis:'',treatment:'other',confirmed:[],evidence:'',sourceAmount:null,sourcePeriod:period,sourceCurrency:currency,source:'Introdução directa; projecção, não declaração submetida'};}
export function salesBook(draft:Record<string,unknown>):SalesBook {
const store=readAcquisitionStore(draft.modelo_a_snapshot) as Store;
if(store.salesOperations!==undefined&&(!store.salesOperations||typeof store.salesOperations!=='object'||Array.isArray(store.salesOperations)))throw new Error('Formato das operações IVA não recuperável; dados preservados, sem substituição.');
const book=store.salesOperations?.[acquisitionScope(draft)];
if(!book)return {currency:String(draft.currency??''),rows:[],periods:[]};
if(typeof book.currency!=='string'||!Array.isArray(book.rows)||!Array.isArray(book.periods)||book.rows.some(r=>!r||Object.keys(newSalesOperation('','','')).some(k=>k==='confirmed'?!Array.isArray(r.confirmed)||r.confirmed.some(c=>!Number.isInteger(c)):k==='sourceAmount'?!(r.sourceAmount===null||typeof r.sourceAmount==='string'):typeof r[k as keyof SalesOperation]!=='string'))||book.periods.some(p=>!p||!/^\d{4}-\d{2}$/.test(p.period)||typeof p.complete!=='boolean'))throw new Error('Operações IVA não recuperáveis; os dados originais foram preservados.');
return book;
}
export function saveSalesBook(draft:Record<string,unknown>,book:SalesBook):string {const store=readAcquisitionStore(draft.modelo_a_snapshot) as Store;return JSON.stringify({...store,salesOperations:{...store.salesOperations,[acquisitionScope(draft)]:book}});}
export function reuseSales(draft:Record<string,unknown>,book:SalesBook):SalesBook {
const source=monthlySalesFromDraft(draft),settings=readMonthlyIvaSettings(String(draft.monthly_iva_settings??''));
if(!/^2026-(0[1-9]|1[0-2])$/.test(settings.start)||Number(settings.start.slice(5))+source.amounts.length>13)throw new Error('Defina o período da projecção dentro de2026 antes de reutilizar vendas.');
if(book.currency!==draft.currency)throw new Error('Moeda da origem divergente; valores preservados, sem conversão.');
const rows=[...book.rows],periods=[...book.periods];
source.amounts.forEach((value,index)=>{const period=`2026-${String(Number(settings.start.slice(5))+index).padStart(2,'0')}`;
if(!periods.some(p=>p.period===period))periods.push({period,complete:false});
if(!rows.some(r=>r.sourcePeriod===period&&r.source!=='Introdução directa; projecção, não declaração submetida'))rows.push({...newSalesOperation(`sales-${period}`,period,book.currency),description:`Vendas do plano — ${period}`,amount:value??'',basis:settings.basis==='unknown'?'':settings.basis,sourceAmount:value,source:source.source,evidence:source.source});
});return {...book,rows,periods};
}
export function splitSales(book:SalesBook,id:string,newId:string):SalesBook {
const row=book.rows.find(r=>r.id===id);if(!row)throw new Error('Operação de origem não encontrada.');
if(row.sourceAmount===null&&!row.splitSource)throw new Error('Reparta uma venda reutilizada com valor de origem determinado; para entradas directas acrescente as operações em falta.');
const source=row.splitSource??{id:row.id,amount:row.sourceAmount??row.amount,period:row.period,currency:row.sourceCurrency,basis:row.basis};
if(!source.basis||source.amount===null||amount(source.amount)===null)throw new Error('Determine o valor e a representação do preço antes de repartir a origem.');
const first={...row,amount:'',sourceAmount:null,splitSource:source,source:`Repartição explícita de ${source.id}: ${source.amount} ${source.currency}; ${row.source}`};
return {...book,rows:[...book.rows.map(r=>r.id===id?first:r),{...first,id:newId,description:'Parcela adicional da mesma venda',treatment:'other',confirmed:[],evidence:''}],periods:book.periods.map(p=>p.period===row.period?{...p,complete:false}:p)};
}
export function recordSales(draft:Record<string,unknown>,book:SalesBook):SalesBook {return {...book,history:book.result?[...(book.history??[]),book.result]:book.history,result:calculateSales(draft,book)};}
function amount(raw:unknown):bigint|null {if(typeof raw!=='string'||!/^\d{1,15}(\.\d{1,6})?$/.test(raw.trim()))return null;const [n,d='']=raw.trim().split('.');return BigInt(n)*1000000n+BigInt(d.padEnd(6,'0'));}
function money(n:bigint):string {return `${n/1000000n}.${(n%1000000n).toString().padStart(6,'0').replace(/0+$/,'').padEnd(2,'0')}`;}
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));}
export function evaluateSalesOperation(row:SalesOperation,currency:string):SalesLineResult {
const rule=operationTreatments.find(r=>r.id===row.treatment),n=amount(row.amount);
const result:SalesLineResult={id:row.id,field:rule?.field??null,net:null,vat:null,gross:null,rate:rule?.rate??null,status:'indeterminate',reason:'',article:rule?.article??'Tratamento desconhecido',source:rule?.source??'Fonte em falta'};
const stop=(reason:string)=>({...result,reason});
if(currency!=='MZN'||row.sourceCurrency!==currency)return stop('Moeda fora de MZN ou divergente da origem; sem conversão silenciosa.');
if(!/^2026-(0[1-9]|1[0-2])$/.test(row.period))return stop('Período fora da cobertura de2026; sem cálculo histórico presumido.');
if(!rule||rule.rate===null||!rule.field)return stop('Operação não calculada: delimite incidência, territorialidade e fundamento desta linha. A remissão do campo06 não constitui uma regra de classificação de todas as operações no estrangeiro.');
if(n===null||!['net','included'].includes(row.basis))return stop('Montante ou indicação de IVA incluído/sem IVA em falta; vazio não significa zero.');
if(!row.description.trim()||!row.establishment.trim())return stop('Indique a operação e o estabelecimento a que pertence; não inferidos da localização do investimento.');
if(rule.conditions.some((_,i)=>!row.confirmed.includes(i))||!row.evidence.trim())return stop('Condições da operação ou fundamento/pressuposto em falta; o sector não prova o tratamento fiscal.');
if(row.sourceAmount!==null&&amount(row.sourceAmount)!==n)return stop('Divergência com o valor reutilizado: conserve a origem e prepare a repartição explicitamente; não substituir silenciosamente.');
if(row.basis==='included'&&rule.rate===5){
const exact={net:fraction(n*20n,21000000n),vat:fraction(n,21000000n),gross:fraction(n,1000000n)};
return {...result,net:fractionPresentation(exact.net).decimal,vat:fractionPresentation(exact.vat).decimal,gross:money(n),exact,status:'calculated',reason:'Base e IVA conservados como fracções exactas (preço×20/21 e preço/21). Decimal a6casas é apresentação técnica, metade para cima; não é regra de arredondamento da declaração nem imposto final a pagar.'};
}
let net=n,vat=0n;
if(rule.rate===16&&row.basis==='included') {net=((n*100n+58000000n)/116000000n)*1000000n;vat=n-net;if(vat<0n)return stop('Arredondamento à unidade produz IVA negativo para este montante; reveja a linha e a documentação.');}
else vat=(n*BigInt(rule.rate)+50n)/100n;
return {...result,net:money(net),vat:money(vat),gross:money(row.basis==='included'?n:net+vat),status:rule.rate===0?'exempt':'calculated',reason:rule.rate===0?(rule.field==='05'?'Isenção com direito à dedução nas condições legais; não é taxa zero nem dedução automática de compras.':'Isenção sem direito à dedução para esta operação; não é isenção geral da entidade.'):'IVA da operação projectada; não é imposto final a pagar nem declaração submetida.'};
}
function key(draft:Record<string,unknown>,book:SalesBook,catalog=operationTreatments.filter(t=>book.rows.some(r=>r.treatment===t.id))):string {return JSON.stringify([operationVersion,catalog,acquisitionScope(draft),draft.currency,draft.province_id,draft.district_id,draft.business_plan_snapshot,draft.cashflow_snapshot,draft.monthly_iva_settings,book.currency,book.rows,book.periods]);}
export function salesCurrent(draft:Record<string,unknown>,book:SalesBook):boolean {return !!book.result&&book.result.version===operationVersion&&book.result.opportunity===acquisitionScope(draft)&&(book.result.key===key(draft,book)||book.result.key===key(draft,book,operationTreatments.filter(t=>legacyV2TreatmentIds.includes(t.id))));}
export function calculateSales(draft:Record<string,unknown>,book:SalesBook,now=new Date().toISOString()):SalesResult {
if(book.currency!=='MZN'||draft.currency!==book.currency)throw new Error('Confirme a moeda original MZN; sem conversão ou reinterpretação.');
if(new Set(book.rows.map(r=>r.id)).size!==book.rows.length||new Set(book.periods.map(p=>p.period)).size!==book.periods.length)throw new Error('Identificadores/períodos duplicados; corrija sem somar em duplicado.');
if(!book.periods.length||book.rows.some(r=>!book.periods.some(p=>p.period===r.period))||book.periods.some(p=>!/^2026-(0[1-9]|1[0-2])$/.test(p.period)))throw new Error('Cada operação deve pertencer a um período válido de2026 incluído na cobertura.');
let live:ReturnType<typeof monthlySalesFromDraft>|null=null;try {live=monthlySalesFromDraft(draft);}catch{ /* Explicitly block stale imported values below. */ }
const start=readMonthlyIvaSettings(String(draft.monthly_iva_settings??'')).start;
const rows=book.rows.map(r=>{
const result=evaluateSalesOperation(r,book.currency);
if(r.splitSource) {
const source=r.splitSource,group=book.rows.filter(v=>v.splitSource?.id===source.id),index=Number(source.period.slice(5))-Number(start.slice(5));
if(group.some(v=>v.period!==source.period||v.basis!==source.basis||v.sourceCurrency!==source.currency||JSON.stringify(v.splitSource)!==JSON.stringify(source))||sum(group.map(v=>v.amount))!==(amount(source.amount)===null?null:money(amount(source.amount)!))||!live||live.amounts[index]!==source.amount)return {...result,exact:undefined,net:null,vat:null,gross:null,status:'indeterminate' as const,reason:'Repartição não reconciliada com o total, período, moeda e representação do preço da origem; nenhuma parcela foi presumida.'};
}
if(r.sourceAmount!==null||r.source.startsWith('Vendas por produto')||r.source.startsWith('Receita mensal')) {
const index=Number(r.sourcePeriod.slice(5))-Number(start.slice(5));
if(!live||!/^2026-/.test(start)||r.sourcePeriod!==r.period||live.amounts[index]!==r.sourceAmount)return {...result,exact:undefined,net:null,vat:null,gross:null,status:'indeterminate' as const,reason:'A origem/período das vendas mudou. Reveja e confirme a nova origem; o resultado anterior foi conservado.'};
}
return result;
});
const periods=book.periods.map(p=>{
const matching=book.rows.map((r,i)=>({input:r,result:rows[i]})).filter(r=>r.input.period===p.period),fields:Record<string,string|null>={},exactFields:Record<string,FiscalFraction|null>={},reasons=matching.filter(r=>r.result.status==='indeterminate').map(r=>`${r.input.description||r.input.id}: ${r.result.reason}`);
const known=p.complete&&!matching.some(r=>r.result.status==='indeterminate');
for(const f of ['01','03','05','07'])fields[f]=known?sum(matching.filter(r=>r.result.field===f).map(r=>r.result.net)):null;
for(const [f,base] of [['02','01'],['04','03']])fields[f]=known?sum(matching.filter(r=>r.result.field===base).map(r=>r.result.vat)):null;
fields['06']=known?'0.00':null;
if(matching.some(r=>r.result.exact)) {
for(const [f,base,part] of [['01','01','net'],['02','01','vat'],['03','03','net'],['04','03','vat'],['05','05','net'],['06','06','net'],['07','07','net']] as const) {
exactFields[f]=known?addFractions(matching.filter(r=>r.result.field===base).map(r=>r.result.exact?.[part]??decimalFraction(r.result[part]!))):null;
fields[f]=exactFields[f]?fractionPresentation(exactFields[f]!).decimal:null;
}
}
if(!p.complete)reasons.push('Confirme que todas as operações do período foram incluídas; uma lista vazia só é zero após essa confirmação.');
return {period:p.period,fields,...(Object.keys(exactFields).length?{exactFields}:{}),reasons};
});
const labels=draft.selection_labels as Record<string,unknown>|undefined;
return {version:operationVersion,revision:(book.result?.revision??0)+1,calculatedAt:now,opportunity:acquisitionScope(draft),location:['province_id','district_id'].filter(k=>draft[k]).map(k=>String(labels?.[k]??draft[k])).join(' / '),currency:book.currency,key:key(draft,book),inputs:structuredClone(book.rows),rows,periods,rounding:'Base líquida: precisão técnica6decimais, metade para cima, sem arredondamento final da declaração. Preço geral com IVA: base=preço×100/116, arredondada à unidade mais próxima (metade para cima), IVA=preço−base, CIVA art.34 redacção Lei10/2025. Preço incluído5%: fracções exactas preço×20/21 e preço/21 conservadas; decimal a6casas/metade para cima apenas na apresentação. A regra declarativa não é presumida.'};
}
export function salesReportRows(draft:Record<string,unknown>):Array<[string,string,string,string]> {
try {const book=salesBook(draft),r=book.result;if(!r)return [];
const context=`${r.currency}; oportunidade ${r.opportunity}; ${r.location||'localização não indicada'}; ${r.version}; revisão ${r.revision}; ${r.calculatedAt}`;
const out:Array<[string,string,string,string]>=[['IVA mensal','Operações IVA L1',salesCurrent(draft,book)?'Resultado correspondente às entradas':'Histórico desactualizado; não aplicar à projecção actual',context],['IVA mensal','Método e limites L1',r.rounding,'Sem apuramento integral, dedução automática ou submissão AT']];
for(const row of r.inputs){const value=r.rows.find(v=>v.id===row.id)!;out.push(['IVA mensal',`${row.period} — ${row.description} — ${row.establishment}`,`Declarado ${row.amount||'em falta'} ${row.sourceCurrency}; ${row.basis==='included'?'IVA incluído':row.basis==='net'?'sem IVA':'base desconhecida'}; líquido ${value.net??'Indeterminado'}; IVA ${value.vat??'Indeterminado'}; bruto ${value.gross??'Indeterminado'}; campo ${value.field??'não determinado'}; ${value.reason}`,`${context}; ${value.article}; ${value.source}; origem: ${row.source}; pressuposto/documento: ${row.evidence||'em falta'}; condições confirmadas: ${row.confirmed.join(',')||'nenhuma'}`]);}
for(const value of r.rows.filter(v=>v.exact))out.push(['IVA mensal',`Precisão exacta — ${value.id}`,`Líquido ${fractionLabel(value.exact!.net)} MZN; IVA ${fractionLabel(value.exact!.vat)} MZN; bruto ${fractionLabel(value.exact!.gross)} MZN`,`${context}; apresentação decimal a6casas, aproximação quando a fracção não termina; não aplicar à declaração sem a sua regra própria.`]);
for(const p of r.periods){out.push(['IVA mensal',`Operações L1 — ${p.period}`,Object.entries(p.fields).map(([f,v])=>`${f}: ${v??'Indeterminado'}`).join('; '),`${context}; ${p.reasons.join(' ')}`]);
if(p.exactFields)out.push(['IVA mensal',`Precisão exacta do período — ${p.period}`,Object.entries(p.exactFields).map(([f,v])=>`${f}: ${v?fractionLabel(v):'Indeterminado'}`).join('; '),`${context}; totais em fracções MZN, calculados antes da apresentação decimal, sem regra declarativa presumida.`]);
}return out;
}catch{return [['IVA mensal','Operações IVA L1','Não foi possível recuperar as operações guardadas','Originais preservados; sem substituição por zero']];}
}
+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.';
+10 -2
View File
@@ -3,10 +3,13 @@ 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 { salesReportRows } from './iva-operations.ts';
import { fiscalQaRows } from './fiscal-qa-preview.ts';
import { csvCellWithDelimiter } from './csv.js';
import {
restoreCashflowDraftSnapshot,
cashflowCurrency,
projectMonthlyCashflow,
} from './monthly-cashflow.ts';
import {
@@ -613,12 +616,15 @@ export function buildProjectDossierRows(
'Planeado/realizado declarado pelo utilizador',
]);
}
rows.push(...acquisitionReportRows(draft));
const operationRows = salesReportRows(draft);
rows.push(...operationRows);
if (draft.monthly_iva_snapshot) {
const saved = readMonthlyIvaRecord(String(draft.monthly_iva_snapshot));
let current = false;
try { current = !!saved && monthlyIvaKey(monthlyIvaInput(draft, readMonthlyIvaSettings(String(draft.monthly_iva_settings ?? '')))) === monthlyIvaKey(saved.input); } catch { /* An old fiscal version is never silently made current. */ }
rows.push(['IVA mensal', 'Estado da versão', current ? 'Entradas correspondentes à projecção guardada' : 'Histórico desactualizado face às vendas ou pressupostos actuais; não aplicar como resultado actual', 'Execução versionada no servidor; sem apuramento final']);
if (saved?.execution.result_snapshot) for (const [label,value] of fiscalQaRows(saved.execution.result_snapshot)) rows.push(['IVA mensal', label, value, 'Snapshot fiscal preservado; sem recálculo na exportação']);
if (saved?.execution.result_snapshot) for (const [label,value] of fiscalQaRows(saved.execution.result_snapshot)) rows.push(['IVA mensal', operationRows.length ? `Execução geral separada — ${label}` : label, value, operationRows.length ? 'Snapshot geral conservado como execução separada; não somar nem substituir a classificação por operação L1. Sem recálculo na exportação.' : 'Snapshot fiscal preservado; sem recálculo na exportação']);
else rows.push(['IVA mensal', 'Resultado', 'Indeterminado ou execução ainda não concluída', 'Sem substituição por zero']);
try {
for (const purchase of modeloAPurchaseReferences(draft)) rows.push(['IVA mensal', `Compras/encomendas — mês ${purchase.month}`, purchase.declared ?? 'Sem informação', 'Pressuposto actual do plano, não resultado fiscal histórico; base/IVA incluído por classificar; IVA suportado e dedutível indeterminados; não é data de factura ou pagamento']);
@@ -637,7 +643,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.',
+31
View File
@@ -0,0 +1,31 @@
<?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 IvaHealthGoodsDraftTest extends TestCase
{
use DatabaseTransactions;
public function test_health_goods_round_trip_preserves_other_opportunity(): void
{
$target = DB::selectOne('select current_database() as name,inet_server_port() as port');
$this->assertSame('simulator_fiscal_delta_20261021_test', $target->name);
$this->assertSame(55441, (int) $target->port);
config(['investor-path.enabled' => true, 'suitecrm.enabled' => false]);
$this->actingAs(User::factory()->create(['email_verified_at' => now()]));
$draft = json_decode(file_get_contents(getenv('IVA_G2A_INPUT') ?: base_path('outputs/iva-l1-g2-health-20261024/calculated.json')), true, flags: JSON_THROW_ON_ERROR);
$id = $this->postJson('/api/v1/simulation-drafts', ['personal_snapshot' => [], 'economic_snapshot' => $draft], ['Idempotency-Key' => (string) Str::uuid()])->assertCreated()->json('data.id');
$recovered = $this->getJson('/api/v1/simulation-drafts/'.$id)->assertOk()->json('data.economic_snapshot');
$this->assertSame($draft['modelo_a_snapshot'], $recovered['modelo_a_snapshot']);
$book = json_decode($recovered['modelo_a_snapshot'], true)['salesOperations']['opportunity:309'];
$this->assertCount(5, $book['result']['rows']);
$this->assertSame('500.00', $book['result']['periods'][0]['fields']['07']);
$this->assertSame(json_decode($draft['modelo_a_snapshot'], true)['salesOperations']['opportunity:310'], json_decode($recovered['modelo_a_snapshot'], true)['salesOperations']['opportunity:310']);
foreach ($book['result']['rows'] as $row) $this->assertSame('0.00', $row['vat']);
if (getenv('IVA_G2A_RECOVERED')) file_put_contents(getenv('IVA_G2A_RECOVERED'), json_encode($recovered, JSON_THROW_ON_ERROR | JSON_UNESCAPED_UNICODE));
}
}
+47
View File
@@ -0,0 +1,47 @@
<?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 IvaOperationsDraftTest extends TestCase
{
use DatabaseTransactions;
public function test_l1_ui_payload_round_trips_without_duplicate_drafts_or_new_rules_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;
$this->actingAs(User::factory()->create(['email_verified_at' => now()]));
$path = getenv('IVA_L1_UI_INPUT') ?: base_path('tests/frontend/fixtures/iva-operations.json');
$draft = json_decode(file_get_contents($path), true, flags: JSON_THROW_ON_ERROR);
$draft['purpose'] = 'simulate_only';
$key = (string) Str::uuid();
$payload = ['personal_snapshot' => [], 'economic_snapshot' => $draft];
$created = $this->postJson('/api/v1/simulation-drafts', $payload, ['Idempotency-Key' => $key])->assertCreated();
$id = $created->json('data.id');
$retry = $this->postJson('/api/v1/simulation-drafts', $payload, ['Idempotency-Key' => $key]);
$this->assertSame($id, $retry->json('data.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']);
$this->assertSame($draft['context_opportunity'], $economic['context_opportunity']);
$store = json_decode($economic['modelo_a_snapshot'], true, flags: JSON_THROW_ON_ERROR);
$book = $store['salesOperations'][$draft['context_opportunity']];
$this->assertSame('iva-operations-l1/1', $book['result']['version']);
$this->assertCount(12, $book['result']['rows']);
$this->assertSame('160.00', $book['result']['rows'][0]['vat']);
$this->assertNull($book['result']['rows'][10]['vat']);
if (getenv('IVA_L1_RECOVERED_OUT')) file_put_contents(getenv('IVA_L1_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);
}
}
+38
View File
@@ -0,0 +1,38 @@
<?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 IvaRationalDraftTest extends TestCase
{
use DatabaseTransactions;
public function test_periodic_results_and_accepted_first_opportunity_round_trip_exactly(): 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]);
$this->actingAs(User::factory()->create(['email_verified_at' => now()]));
$path = getenv('IVA_RATIONAL_INPUT') ?: base_path('tests/frontend/fixtures/iva-rational.json');
$draft = json_decode(file_get_contents($path), true, flags: JSON_THROW_ON_ERROR);
$store = json_decode($draft['modelo_a_snapshot'], true, flags: JSON_THROW_ON_ERROR);
$first = $store['salesOperations']['opportunity:309'];
$id = $this->postJson('/api/v1/simulation-drafts', ['personal_snapshot' => [], 'economic_snapshot' => $draft], ['Idempotency-Key' => (string) Str::uuid()])->assertCreated()->json('data.id');
$economic = $this->getJson('/api/v1/simulation-drafts/'.$id)->assertOk()->json('data.economic_snapshot');
$this->assertSame($draft['modelo_a_snapshot'], $economic['modelo_a_snapshot']);
$saved = json_decode($economic['modelo_a_snapshot'], true, flags: JSON_THROW_ON_ERROR);
$this->assertSame($first, $saved['salesOperations']['opportunity:309']);
$second = $saved['salesOperations']['opportunity:310']['result'];
$this->assertSame('iva-operations-l1/2', $second['version']);
$this->assertSame(['numerator' => '20', 'denominator' => '21'], $second['rows'][0]['exact']['net']);
$this->assertSame(['numerator' => '1', 'denominator' => '21'], $second['rows'][0]['exact']['vat']);
$this->assertSame('0.047619', $second['rows'][0]['vat']);
if (getenv('IVA_RATIONAL_RECOVERED')) file_put_contents(getenv('IVA_RATIONAL_RECOVERED'), json_encode($economic, JSON_THROW_ON_ERROR | JSON_UNESCAPED_UNICODE));
}
}
@@ -0,0 +1,30 @@
<?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 IvaServicesGroupDraftTest extends TestCase
{
use DatabaseTransactions;
public function test_services_group_round_trips_with_conditions_and_source(): void
{
$target = DB::selectOne('select current_database() as name,inet_server_port() as port');
$this->assertSame('simulator_fiscal_delta_20261021_test', $target->name);
$this->assertSame(55441, (int) $target->port);
config(['investor-path.enabled' => true, 'suitecrm.enabled' => false]);
$this->actingAs(User::factory()->create(['email_verified_at' => now()]));
$draft = json_decode(file_get_contents(getenv('IVA_G1_INPUT') ?: base_path('outputs/iva-l1-groups-20261024/calculated.json')), true, flags: JSON_THROW_ON_ERROR);
$id = $this->postJson('/api/v1/simulation-drafts', ['personal_snapshot' => [], 'economic_snapshot' => $draft], ['Idempotency-Key' => (string) Str::uuid()])->assertCreated()->json('data.id');
$recovered = $this->getJson('/api/v1/simulation-drafts/'.$id)->assertOk()->json('data.economic_snapshot');
$this->assertSame($draft['modelo_a_snapshot'], $recovered['modelo_a_snapshot']);
$book = json_decode($recovered['modelo_a_snapshot'], true)['salesOperations']['opportunity:310'];
$this->assertCount(4, $book['result']['rows']);
$this->assertSame('400.00', $book['result']['periods'][0]['fields']['07']);
foreach ($book['result']['rows'] as $row) $this->assertSame('0.00', $row['vat']);
if (getenv('IVA_G1_RECOVERED')) file_put_contents(getenv('IVA_G1_RECOVERED'), json_encode($recovered, JSON_THROW_ON_ERROR | JSON_UNESCAPED_UNICODE));
}
}
+32
View File
@@ -0,0 +1,32 @@
<?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 IvaSocialDraftTest extends TestCase
{
use DatabaseTransactions;
public function test_social_context_preserves_both_accepted_opportunities(): void
{
$target = DB::selectOne('select current_database() as name,inet_server_port() as port');
$this->assertSame('simulator_fiscal_delta_20261021_test', $target->name);
$this->assertSame(55441, (int) $target->port);
config(['investor-path.enabled' => true, 'suitecrm.enabled' => false]);
$this->actingAs(User::factory()->create(['email_verified_at' => now()]));
$draft = json_decode(file_get_contents(getenv('IVA_G2B_INPUT') ?: base_path('outputs/iva-l1-g2-social-20261024/calculated.json')), true, flags: JSON_THROW_ON_ERROR);
$id = $this->postJson('/api/v1/simulation-drafts', ['personal_snapshot' => [], 'economic_snapshot' => $draft], ['Idempotency-Key' => (string) Str::uuid()])->assertCreated()->json('data.id');
$recovered = $this->getJson('/api/v1/simulation-drafts/'.$id)->assertOk()->json('data.economic_snapshot');
$this->assertSame($draft['modelo_a_snapshot'], $recovered['modelo_a_snapshot']);
$book = json_decode($recovered['modelo_a_snapshot'], true)['salesOperations']['opportunity:311'];
$this->assertCount(6, $book['result']['rows']);
$this->assertSame('600.00', $book['result']['periods'][0]['fields']['07']);
$this->assertSame(json_decode($draft['modelo_a_snapshot'], true)['salesOperations']['opportunity:310'], json_decode($recovered['modelo_a_snapshot'], true)['salesOperations']['opportunity:310']);
$this->assertSame(json_decode($draft['modelo_a_snapshot'], true)['salesOperations']['opportunity:309'], json_decode($recovered['modelo_a_snapshot'], true)['salesOperations']['opportunity:309']);
foreach ($book['result']['rows'] as $row) $this->assertSame('0.00', $row['vat']);
if (getenv('IVA_G2B_RECOVERED')) file_put_contents(getenv('IVA_G2B_RECOVERED'), json_encode($recovered, JSON_THROW_ON_ERROR | JSON_UNESCAPED_UNICODE));
}
}
@@ -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']);
}
}
File diff suppressed because one or more lines are too long
@@ -0,0 +1,9 @@
import fs from 'node:fs';
import {newSalesOperation,recordSales,saveSalesBook} from '../../../resources/js/lib/iva-operations.ts';
export function l1DeltaFixture(){
const draft=JSON.parse(fs.readFileSync(new URL('iva-operations.json',import.meta.url),'utf8'));
draft.context_opportunity='opportunity:310';draft.project_title='SYNTHETIC QA — L1 precisão racional';
const context=JSON.parse(draft.context_applicability_snapshot);context.opportunity_choices.push({opportunity:{key:'opportunity:310',title:'SYNTHETIC QA — segunda oportunidade racional'}});draft.context_applicability_snapshot=JSON.stringify(context);
let book={currency:'MZN',rows:['1','100','0.02'].map((amount,i)=>({...newSalesOperation(`rational-${i}`,'2026-01','MZN'),amount,description:`SYNTHETIC QA periódico ${i+1}`,establishment:'SYNTHETIC QA estabelecimento',basis:'included',treatment:'private_health',confirmed:[0,1],evidence:'SYNTHETIC QA — pressupostos já delimitados do tratamento, preço incluído'})),periods:[{period:'2026-01',complete:true}]};
book=recordSales(draft,book);draft.modelo_a_snapshot=saveSalesBook(draft,book);return {draft,book};
}
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -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));}
+22
View File
@@ -0,0 +1,22 @@
import assert from 'node:assert/strict';
import test from 'node:test';
import fs from 'node:fs';
import {operationTreatments,newSalesOperation,evaluateSalesOperation,recordSales,saveSalesBook,salesBook,salesCurrent} from '../../resources/js/lib/iva-operations.ts';
import {createProjectDossierReport} from '../../resources/js/lib/project-dossier-report.ts';
const ids=['financial_stamp','insurance_stamp','public_waste','public_funeral'];
function fixture(){const draft={purpose:'simulate_only',currency:'MZN',context_opportunity:'opportunity:310',project_title:'SYNTHETIC QA — G1 serviços isentos',province_id:'cf5e81f1-ffbc-535d-b1b6-b44b445c4421',selection_labels:{province_id:'Manica'}};
let book={currency:'MZN',rows:ids.map((id,i)=>({...newSalesOperation(`g1-${i}`,'2026-01','MZN'),description:`SYNTHETIC QA ${id}`,establishment:'SYNTHETIC QA estabelecimento',amount:'100',basis:'net',treatment:id,confirmed:[0,1],evidence:'SYNTHETIC QA — condições documentadas pressupostas, sem certificação'})),periods:[{period:'2026-01',complete:true}]};book=recordSales(draft,book);draft.modelo_a_snapshot=saveSalesBook(draft,book);return {draft,book};}
test('G1 four documented operations use the same conditions and persistence, with independent applicability',()=>{
const {draft,book}=fixture();for(const row of book.rows){const r=evaluateSalesOperation(row,'MZN');assert.equal(r.vat,'0.00');assert.equal(r.field,'07');assert.match(r.article,/9\(/);assert.match(r.source,/Lei22/);assert.equal(evaluateSalesOperation({...row,confirmed:[0]},'MZN').vat,null);assert.equal(evaluateSalesOperation({...row,evidence:''},'MZN').vat,null);}
assert.equal(book.result.periods[0].fields['07'],'400.00');const recovered=JSON.parse(JSON.stringify(draft));assert.equal(salesCurrent(recovered,salesBook(recovered)),true);assert.match(JSON.stringify(createProjectDossierReport(recovered,{selectedSections:new Set(['finance'])})),/400\.00/);
assert.equal(salesCurrent({...recovered,currency:'USD'},salesBook(recovered)),false);
assert.equal(salesCurrent({...recovered,context_opportunity:'opportunity:309'},salesBook(recovered)),false);
const rule=operationTreatments.find(t=>t.id==='financial_stamp');const source=rule.source;
try{rule.source+=' — changed source';assert.equal(salesCurrent(recovered,salesBook(recovered)),false);}finally{rule.source=source;}
});
test('adding independent catalogue rules does not invalidate or recalculate accepted rational snapshots',()=>{
const draft=JSON.parse(fs.readFileSync(new URL('./fixtures/iva-rational.json',import.meta.url),'utf8'));const original=draft.modelo_a_snapshot;assert.equal(salesCurrent(draft,salesBook(draft)),true);assert.equal(draft.modelo_a_snapshot,original);assert.equal(operationTreatments.filter(t=>ids.includes(t.id)).length,4);
operationTreatments.push({...operationTreatments[0],id:'synthetic_future_independent'});
try{assert.equal(salesCurrent(draft,salesBook(draft)),true);assert.equal(draft.modelo_a_snapshot,original);}finally{operationTreatments.pop();}
});
if(process.env.IVA_G1_OUT)fs.writeFileSync(process.env.IVA_G1_OUT,JSON.stringify(fixture().draft,null,2));
+32
View File
@@ -0,0 +1,32 @@
import assert from 'node:assert/strict';
import test from 'node:test';
import fs from 'node:fs';
import {operationTreatments,newSalesOperation,evaluateSalesOperation,recordSales,saveSalesBook,salesBook,salesCurrent} from '../../resources/js/lib/iva-operations.ts';
import {createProjectDossierReport} from '../../resources/js/lib/project-dossier-report.ts';
const ids=['health_assistive','human_materials','patient_transport','mosquito_nets','medicines'];
export function healthFixture(){
const draft=JSON.parse(fs.readFileSync(new URL('./fixtures/iva-services.json',import.meta.url),'utf8'));
draft.context_opportunity='opportunity:309';draft.project_title='SYNTHETIC QA — G2A saúde';
let book={currency:'MZN',rows:ids.map((id,i)=>({...newSalesOperation(`g2a-${i}`,'2026-02','MZN'),description:`SYNTHETIC QA ${id}`,establishment:'SYNTHETIC QA estabelecimento Manica',amount:'100',basis:i%2?'included':'net',treatment:id,confirmed:operationTreatments.find(t=>t.id===id).conditions.map((_,n)=>n),evidence:'SYNTHETIC QA — natureza, destino e autorização quando aplicável pressupostos documentados; sem certificação'})),periods:[{period:'2026-02',complete:true}]};
book=recordSales(draft,book);draft.modelo_a_snapshot=saveSalesBook(draft,book);return {draft,book};
}
test('G2A five health branches require their own nature, location, purpose and authorization, not the health sector',()=>{
const {book}=healthFixture();
for(const row of book.rows){const r=evaluateSalesOperation(row,'MZN');assert.equal(r.field,'07');assert.equal(r.net,'100.00');assert.equal(r.vat,'0.00');assert.equal(r.gross,'100.00');assert.match(r.source,/1426\(267\)/);
const count=operationTreatments.find(t=>t.id===row.treatment).conditions.length;
for(let missing=0;missing<count;missing++)assert.equal(evaluateSalesOperation({...row,confirmed:row.confirmed.filter(n=>n!==missing)},'MZN').vat,null);
assert.equal(evaluateSalesOperation({...row,evidence:''},'MZN').vat,null);
}
assert.equal(book.result.periods[0].fields['07'],'500.00');
});
test('G2A reuses saved dossier data, retains G1 unchanged, and reports recovered context without recalculating accepted treatments',()=>{
const original=JSON.parse(fs.readFileSync(new URL('./fixtures/iva-services.json',import.meta.url),'utf8'));
const before=JSON.parse(original.modelo_a_snapshot).salesOperations['opportunity:310'];
const {draft}=healthFixture();const recovered=JSON.parse(JSON.stringify(draft));assert.equal(salesCurrent(recovered,salesBook(recovered)),true);
assert.deepEqual(JSON.parse(recovered.modelo_a_snapshot).salesOperations['opportunity:310'],before);
const model=JSON.stringify(createProjectDossierReport(recovered,{selectedSections:new Set(['finance'])}));
for(const token of [...ids,'500.00','opportunity:309','Manica','MZN','2026-02'])assert.ok(model.includes(token),token);
assert.equal(model.includes('financial_stamp'),false);
assert.equal(draft.province_id,original.province_id);assert.deepEqual(draft.selection_labels,original.selection_labels);
});
if(process.env.IVA_G2A_OUT)fs.writeFileSync(process.env.IVA_G2A_OUT,JSON.stringify(healthFixture().draft,null,2));
+40
View File
@@ -0,0 +1,40 @@
import assert from 'node:assert/strict';
import test from 'node:test';
import fs from 'node:fs';
import {l1DeltaFixture} from './fixtures/iva-l1-delta-fixture.mjs';
import {calculateSales,evaluateSalesOperation,modeloA06Correspondence,salesBook,salesCurrent,saveSalesBook} from '../../resources/js/lib/iva-operations.ts';
import {calculateAcquisitions,saveAcquisitionBook} from '../../resources/js/lib/iva-acquisitions.ts';
import {addFractions,decimalFraction,fraction,fractionLabel,fractionPresentation} from '../../resources/js/lib/fiscal-rational.ts';
import {createProjectDossierReport} from '../../resources/js/lib/project-dossier-report.ts';
test('periodic included5% retains exact rational amounts; only presentation is rounded',()=>{
const {book}=l1DeltaFixture(),r=book.result.rows[0];
assert.deepEqual(r.exact.net,{numerator:'20',denominator:'21'});assert.deepEqual(r.exact.vat,{numerator:'1',denominator:'21'});
assert.equal(r.net,'0.952381');assert.equal(r.vat,'0.047619');assert.equal(r.status,'calculated');assert.match(r.reason,/apresentação técnica/);
assert.deepEqual(addFractions([r.exact.net,r.exact.vat]),r.exact.gross);
assert.deepEqual(book.result.rows[1].exact.vat,{numerator:'100',denominator:'21'});
});
test('aggregation uses exact fractions, not21 rounded values',()=>{
const {draft,book}=l1DeltaFixture();book.rows=Array.from({length:21},(_,i)=>({...book.rows[0],id:`q${i}`}));const r=calculateSales(draft,book);
assert.equal(r.rows.every(row=>row.vat==='0.047619'),true);assert.equal(r.periods[0].fields['04'],'1.00');assert.deepEqual(r.periods[0].exactFields['04'],{numerator:'1',denominator:'1'});assert.equal(r.periods[0].fields['03'],'20.00');
});
test('new version round trips by opportunity, keeping accepted first snapshot untouched',()=>{
const accepted=JSON.parse(fs.readFileSync(new URL('./fixtures/iva-operations.json',import.meta.url),'utf8'));const original=JSON.parse(accepted.modelo_a_snapshot).salesOperations['opportunity:309'];
const {draft,book}=l1DeltaFixture();const saved=JSON.parse(JSON.stringify({...draft,modelo_a_snapshot:saveSalesBook(draft,book)}));
assert.deepEqual(JSON.parse(saved.modelo_a_snapshot).salesOperations['opportunity:309'],original);assert.equal(salesCurrent(saved,salesBook(saved)),true);assert.deepEqual(salesBook(saved).result.rows[2].exact,book.result.rows[2].exact);
});
test('conditional14-18 and report carry exact precision rather than rounded line amounts',()=>{
const {draft,book}=l1DeltaFixture();const a={currency:'MZN',rows:[],periods:['2026-01','2026-02','2026-03','2026-04'].map(period=>({period,complete:true,absent:['03','04','05','06','07','11','12','13']}))};
const result=calculateAcquisitions(draft,a);assert.equal(fractionLabel(result.periods[0].exactFields['17']),'5051/1050');assert.equal(result.periods[0].fields['17'],'4.810476');
a.result=result;draft.modelo_a_snapshot=saveAcquisitionBook(draft,a);const report=JSON.stringify(createProjectDossierReport(draft,{selectedSections:new Set(['finance'])}));assert.match(report,/5051\/1050/);assert.match(report,/20\/21/);assert.match(report,/opportunity:310/);
assert.match(report,/apresentação técnica/);assert.equal(book.result.rows.length,3);
});
test('missing inputs still block only the affected result; invalid fractions do not become zero',()=>{
const {book}=l1DeltaFixture();assert.equal(evaluateSalesOperation({...book.rows[0],amount:''},'MZN').vat,null);
assert.equal(evaluateSalesOperation({...book.rows[0],basis:''},'MZN').vat,null);assert.equal(evaluateSalesOperation({...book.rows[0],amount:'0'},'MZN').vat,'0.00');
const {draft}=l1DeltaFixture();book.rows[0].sourceAmount='1';const stale=calculateSales(draft,book).rows[0];assert.equal(stale.vat,null);assert.equal(stale.exact,undefined);
assert.throws(()=>fractionPresentation({numerator:'1',denominator:'0'}));assert.equal(fractionPresentation(fraction(1n,21n)).approximate,true);assert.equal(fractionPresentation(decimalFraction('105')).approximate,false);
});
test('form06 is not automatically inferred from a foreign operation or old stock instructions',()=>{
const {book}=l1DeltaFixture();const r=evaluateSalesOperation({...book.rows[0],treatment:'foreign_operation'},'MZN');assert.equal(r.field,null);assert.equal(r.vat,null);assert.match(modeloA06Correspondence,/existências/);assert.match(modeloA06Correspondence,/separe05\/06/);
});
if(process.env.IVA_L1_DELTA_OUT)fs.writeFileSync(process.env.IVA_L1_DELTA_OUT,JSON.stringify(l1DeltaFixture().draft,null,2));
+74
View File
@@ -0,0 +1,74 @@
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 {calculateSales,evaluateSalesOperation,newSalesOperation,operationTreatments,recordSales,reuseSales,salesBook,salesCurrent,salesReportRows,saveSalesBook,splitSales} from '../../resources/js/lib/iva-operations.ts';
import {acquisitionBook,calculateAcquisitions,newAcquisition,saveAcquisitionBook} from '../../resources/js/lib/iva-acquisitions.ts';
import {createProjectDossierReport} from '../../resources/js/lib/project-dossier-report.ts';
export function operationsFixture(){
const draft=closureFixture().draft;draft.project_title='SYNTHETIC QA — L1 operações IVA';draft.province_id='cf5e81f1-ffbc-535d-b1b6-b44b445c4421';draft.selection_labels={province_id:'Manica'};
const rows=operationTreatments.filter(t=>['general','private_health','private_education','private_training','personal_lessons','export_goods','housing_rent','public_health','public_education','public_training','foreign_operation','other'].includes(t.id)).map((t,i)=>({...newSalesOperation(`qa-${i}`,'2026-01','MZN'),description:`SYNTHETIC QA ${t.label}`,establishment:'SYNTHETIC QA estabelecimento Manica',amount:t.id==='general'?'1161':'100',basis:t.id==='general'?'included':'net',treatment:t.id,confirmed:t.conditions.map((_,i)=>i),evidence:'SYNTHETIC QA — pressupostos de ensaio, não documento real'}));
const book={currency:'MZN',rows,periods:[{period:'2026-01',complete:true}]};return {draft,book};
}
test('each enumerated treatment calculates only with its conditions, article and source',()=>{
const {book}=operationsFixture();
for(const row of book.rows){const r=evaluateSalesOperation(row,'MZN');if(['other','foreign_operation'].includes(row.treatment)){assert.equal(r.vat,null);continue;}
assert.notEqual(r.vat,null,row.treatment);assert.ok(r.article.includes('CIVA'));assert.ok(r.source.includes('Lei'));
assert.equal(evaluateSalesOperation({...row,confirmed:[]},'MZN').vat,null,row.treatment);
assert.equal(evaluateSalesOperation({...row,evidence:''},'MZN').vat,null);
}
const by=Object.fromEntries(book.rows.map(r=>[r.treatment,evaluateSalesOperation(r,'MZN')]));assert.equal(by.private_health.vat,'5.00');assert.equal(by.export_goods.field,'05');assert.match(by.export_goods.reason,/não é taxa zero/);assert.equal(by.housing_rent.field,'07');
});
test('VAT-included general uses statutory unit rounding, conserves gross; included5percent presentation is covered by the rational delta',()=>{
const row=operationsFixture().book.rows[0];const r=evaluateSalesOperation(row,'MZN');assert.equal(r.net,'1001.00');assert.equal(r.vat,'160.00');assert.equal(r.gross,'1161.00');
assert.equal(evaluateSalesOperation({...row,amount:'1160'},'MZN').net,'1000.00');
assert.equal(evaluateSalesOperation({...row,basis:'net',amount:'1000.03'},'MZN').vat,'160.0048');
assert.equal(evaluateSalesOperation({...row,basis:'',amount:'0'},'MZN').vat,null);
assert.equal(evaluateSalesOperation({...row,amount:'0'},'MZN').vat,'0.00');
const reduced=operationsFixture().book.rows[1];assert.equal(evaluateSalesOperation({...reduced,basis:'included'},'MZN').vat,'4.761905');
assert.equal(evaluateSalesOperation({...reduced,basis:'included',amount:'105'},'MZN').net,'100.00');
assert.equal(evaluateSalesOperation({...reduced,basis:'included',amount:'105'},'MZN').vat,'5.00');
});
test('missing information, invalid dates/currency and legal ambiguity never become zero',()=>{
const row=operationsFixture().book.rows[0];for(const patch of [{amount:''},{amount:'-1'},{period:'2025-12'},{period:'2026-13'},{establishment:''},{sourceCurrency:'USD'},{treatment:'invented'}])assert.equal(evaluateSalesOperation({...row,...patch},'MZN').vat,null);
const {draft,book}=operationsFixture();book.result=calculateSales(draft,book);assert.equal(book.result.rows[0].vat,'160.00');assert.equal(book.result.periods[0].fields['02'],null);assert.match(book.result.periods[0].reasons.join(' '),/campo06/);
book.rows=book.rows.filter(r=>!['other','foreign_operation'].includes(r.treatment));book.result=calculateSales(draft,book);assert.equal(book.result.periods[0].fields['04'],'20.00');assert.equal(book.result.periods[0].fields['07'],'400.00');assert.equal(book.result.periods[0].fields['05'],'100.00');
book.periods[0].complete=false;assert.equal(calculateSales(draft,book).periods[0].fields['02'],null);
book.rows=[];book.periods[0].complete=true;assert.equal(calculateSales(draft,book).periods[0].fields['02'],'0.00');
});
test('reuse preserves original sales, intentional edits and period; drift makes new calculation indeterminate',()=>{
const {draft}=operationsFixture();let book=reuseSales(draft,salesBook(draft));assert.equal(book.rows[0].amount,'1000.03');assert.equal(book.rows[3].amount,'');assert.equal(book.rows[0].treatment,'other');
book.rows[0].description='Edited';book=reuseSales(draft,book);assert.equal(book.rows.length,4);assert.equal(book.rows[0].description,'Edited');
const template=operationsFixture().book.rows[0];book.rows[0]={...template,...book.rows[0],treatment:'general',confirmed:[0,1],establishment:'QA',evidence:'QA'};assert.notEqual(calculateSales(draft,book).rows[0].vat,null);
book.rows[0].amount='900';assert.equal(calculateSales(draft,book).rows[0].vat,null);
assert.throws(()=>reuseSales({...draft,currency:'USD'},book),/moeda/i);
});
test('opportunity-specific persistence, report and stale history preserve the first when only the second changes',()=>{
const {draft,book}=operationsFixture();draft.context_opportunity='opportunity:1';book.result=calculateSales(draft,book,'2026-10-24T15:00:00Z');draft.modelo_a_snapshot=saveSalesBook(draft,book);const first=JSON.stringify(salesBook(draft));
draft.context_opportunity='opportunity:309';assert.equal(salesBook(draft).rows.length,0);const second=operationsFixture().book;second.rows[0].amount='2320';second.result=calculateSales(draft,second);draft.modelo_a_snapshot=saveSalesBook(draft,second);
const recovered=JSON.parse(JSON.stringify(draft));assert.equal(salesCurrent(recovered,salesBook(recovered)),true);assert.equal(JSON.stringify(salesBook({...recovered,context_opportunity:'opportunity:1'})),first);
assert.match(JSON.stringify(createProjectDossierReport(recovered,{selectedSections:new Set(['finance'])})),/2320/);assert.match(JSON.stringify(salesReportRows(recovered)),/Manica/);
second.rows[0].amount='1';draft.modelo_a_snapshot=saveSalesBook(draft,second);assert.equal(salesCurrent(draft,second),false);assert.match(JSON.stringify(salesReportRows(draft)),/Histórico/);
assert.throws(()=>salesBook({...draft,modelo_a_snapshot:'broken'}));
assert.throws(()=>salesBook({...draft,modelo_a_snapshot:JSON.stringify({version:1,books:{},salesOperations:[]})}),/preservados/);
});
test('acquisitions use current classified sales, refuse stale absence and do not universalise deduction',()=>{
const {draft,book}=operationsFixture();book.rows=book.rows.filter(r=>!['other','foreign_operation'].includes(r.treatment));book.result=calculateSales(draft,book);draft.modelo_a_snapshot=saveSalesBook(draft,book);
const a={currency:'MZN',rows:[],periods:['2026-01','2026-02','2026-03','2026-04'].map(period=>({period,complete:true,absent:['03','04','05','06','07','11','12','13']}))};
const result=calculateAcquisitions(draft,a);assert.equal(result.periods[0].fields['04'],'20.00');assert.equal(result.periods[0].fields['14'],'1901.00');assert.equal(result.periods[0].fields['16'],'180.00');
a.result=result;draft.modelo_a_snapshot=saveAcquisitionBook(draft,a);assert.equal(salesCurrent(draft,salesBook(draft)),true);assert.equal(acquisitionBook(draft).result.periods[0].fields['04'],'20.00');
a.rows=[{...newAcquisition('new-context-purchase','2026-01','MZN'),net:'100',vat:'16',gross:'116',documentType:'invoice',documentRef:'SYNTHETIC QA',documentDate:'2026-01-10',supplier:'SYNTHETIC QA',supplierRegistered:'yes',legalDocument:'yes',held:'yes',domestic:'yes',allocation:'taxable_only',exclusion:'none',integrity:'yes'}];a.taxpayerRegistered='yes';
assert.equal(calculateAcquisitions(draft,a).periods[0].fields['09'],null);
a.rows[0].allocation='no_right';assert.equal(calculateAcquisitions(draft,a).periods[0].fields['09'],'0.00');a.rows=[];
book.rows[0].amount='116';draft.modelo_a_snapshot=saveSalesBook(draft,book);assert.equal(calculateAcquisitions(draft,a).periods[0].fields['02'],null);
});
test('split source reconciles every parcel and recalculation keeps the prior frozen version',()=>{
const {draft}=operationsFixture();let book=reuseSales(draft,salesBook(draft));book=splitSales(book,book.rows[0].id,'second-part');
const group=book.rows.filter(r=>r.splitSource);assert.equal(group.length,2);
for(const [i,r] of group.entries())Object.assign(r,{amount:i?'500.03':'500',treatment:'general',confirmed:[0,1],establishment:'QA',evidence:'QA'});
assert.notEqual(calculateSales(draft,book).rows[0].vat,null);
group[1].amount='499';assert.equal(calculateSales(draft,book).rows[0].vat,null);
group[1].amount='500.03';book=recordSales(draft,book);const frozen=JSON.stringify(book.result);book=recordSales(draft,book);assert.equal(JSON.stringify(book.history[0]),frozen);assert.equal(book.result.revision,2);
});
if(process.env.IVA_L1_FIXTURE_OUT){const {draft,book}=operationsFixture();book.result=calculateSales(draft,book,'2026-10-24T15:00:00Z');draft.modelo_a_snapshot=saveSalesBook(draft,book);fs.writeFileSync(process.env.IVA_L1_FIXTURE_OUT,JSON.stringify(draft,null,2));}
+28
View File
@@ -0,0 +1,28 @@
import assert from 'node:assert/strict';
import test from 'node:test';
import fs from 'node:fs';
import {operationTreatments,newSalesOperation,evaluateSalesOperation,recordSales,saveSalesBook,salesBook,salesCurrent} from '../../resources/js/lib/iva-operations.ts';
import {createProjectDossierReport} from '../../resources/js/lib/project-dossier-report.ts';
const ids=['social_assistance_public','social_assistance_nonprofit','social_care_public','social_care_nonprofit','activity_facilities_public','activity_facilities_nonprofit'];
function seed(){return JSON.parse(fs.readFileSync(new URL('./fixtures/iva-health-goods.json',import.meta.url),'utf8'));}
function fixture(){const draft=seed();draft.context_opportunity='opportunity:311';draft.project_title='SYNTHETIC QA — G2B social';
let book={currency:'MZN',rows:ids.map((id,i)=>({...newSalesOperation(`g2b-${i}`,'2026-03','MZN'),description:`SYNTHETIC QA ${id}`,establishment:'SYNTHETIC QA equipamento Manica',amount:'100',basis:i%2?'included':'net',treatment:id,confirmed:operationTreatments.find(t=>t.id===id).conditions.map((_,n)=>n),evidence:'SYNTHETIC QA — natureza, entidade, reconhecimento e condições cumulativas pressupostos documentados; sem certificação'})),periods:[{period:'2026-03',complete:true}]};book=recordSales(draft,book);draft.modelo_a_snapshot=saveSalesBook(draft,book);return {draft,book};}
test('G2B three branches have six entity-specific treatments and cumulative nonprofit conditions',()=>{
const {book}=fixture();for(const row of book.rows){const rule=operationTreatments.find(t=>t.id===row.treatment),r=evaluateSalesOperation(row,'MZN');assert.equal(r.field,'07');assert.equal(r.net,'100.00');assert.equal(r.vat,'0.00');assert.equal(r.gross,'100.00');assert.match(r.article,/10\(/);
assert.equal(rule.conditions.length,row.treatment.endsWith('_nonprofit')?7:4);
for(const missing of row.confirmed)assert.equal(evaluateSalesOperation({...row,confirmed:row.confirmed.filter(n=>n!==missing)},'MZN').vat,null);
assert.equal(evaluateSalesOperation({...row,evidence:''},'MZN').vat,null);
}
assert.equal(book.result.periods[0].fields['07'],'600.00');
});
test('new social context round-trips and reports only its operations, preserving both accepted books without recalculation',()=>{
const {draft,book}=fixture(),original=seed();const old=JSON.parse(original.modelo_a_snapshot).salesOperations;
const recovered=JSON.parse(JSON.stringify(draft)),store=JSON.parse(recovered.modelo_a_snapshot).salesOperations;
for(const id of ['opportunity:309','opportunity:310']){assert.deepEqual(store[id],old[id]);assert.equal(salesCurrent({...recovered,context_opportunity:id},store[id]),true);}
assert.equal(salesCurrent(recovered,salesBook(recovered)),true);
const model=JSON.stringify(createProjectDossierReport(recovered,{selectedSections:new Set(['finance'])}));
for(const token of [...ids,'600.00','opportunity:311','Manica','2026-03','MZN'])assert.ok(model.includes(token),token);
for(const token of ['financial_stamp','patient_transport'])assert.equal(model.includes(token),false);
assert.equal(book.result.rows.length,6);assert.equal(draft.currency,original.currency);assert.deepEqual(draft.selection_labels,original.selection_labels);
});
if(process.env.IVA_G2B_OUT)fs.writeFileSync(process.env.IVA_G2B_OUT,JSON.stringify(fixture().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,
);