Compare commits

...
22 changed files with 4938 additions and 118 deletions
@@ -19,12 +19,21 @@ class InvestmentTerritoryController extends Controller
$data = $request->validate(['territory_id' => ['required', 'uuid']]);
$profile = $request->is('minhas-oportunidades/*');
$management = $request->is('management/*') || $profile;
$actor = $request->user();
$access = app(PortalProfileAccess::class);
// The general consumers use this same endpoint while authenticated.
// Reuse the read access already granted by the private cadastral routes.
$profileAccess = $actor && $actor->hasVerifiedEmail()
&& ($access->grants($actor) !== [] || $access->superuser($actor));
$internal = $profileAccess || ($actor?->hasVerifiedEmail() && $actor->can('manage-portal'));
if ($management) {
$access = app(PortalProfileAccess::class);
abort_unless($profile ? ($access->grants($request->user()) !== [] || $access->superuser($request->user())) : $request->user()?->can('manage-portal'), 403);
abort_unless($profile ? $profileAccess : $internal, 403);
}
return response()->json(app(TerritoryGeometry::class)->context($data['territory_id'], $management));
return response()->json([
...app(TerritoryGeometry::class)->context($data['territory_id'], $internal),
'geometry_scope' => $internal ? 'authorized_internal' : 'public',
])->header('Cache-Control', 'private, no-store');
}
private const TABLES = ['opportunities' => 'opportunities', 'projects' => 'projects', 'clusters' => 'economic_clusters', 'corridors' => 'development_corridors', 'zones' => 'investment_zones'];
@@ -14,12 +14,13 @@ final class EnsureScopedPortalAdministration
public function handle(Request $request, Closure $next): Response
{
$actor = $request->user();
abort_unless($actor, 403);
abort_unless($actor !== null, 403);
$scope = app(ScopedPortalAdministration::class);
$path = $request->path();
$interests = $request->is('management/interests', 'management/interests/*', 'api/v1/investment/admin/interests/*');
// Fiscal maintenance reuses authorised administration; unrelated gates remain unchanged.
$catalogue = $request->is('management/territory-geometry-import', 'management/territory-geometry-import/*',
$geometryRead = $request->isMethod('GET') && $path === 'management/territory-geometries';
$catalogue = $geometryRead || $request->is('management/territory-geometry-import', 'management/territory-geometry-import/*',
'management/economic-references/lot-01', 'management/economic-references/lot-01/*',
'management/education', 'management/education/*', 'management/investment/references', 'management/investment/references/*', 'api/v1/investment/admin/references', 'api/v1/investment/admin/references/*',
'management/investment/zones', 'management/investment/zones/*', 'api/v2/investment/admin/zones', 'api/v2/investment/admin/zones/*',
@@ -0,0 +1,42 @@
<?php
namespace App\Services;
use RuntimeException;
/** Public read projection authorized by Tempo; source rows and originals stay private. */
class ApprovedAdministrativeBoundaries
{
public const HASH = '06ab905fcf9b19115c3521bf81de03f37873880f7765f229b9e1f79d99df7adc';
/** @var array<string,array<string,string>>|null */
private ?array $items = null;
/** @return array<string,array<string,string>> */
public function snapshots(): array
{
if ($this->items !== null) {
return $this->items;
}
$contents = file_get_contents(base_path('docs/investmoz/unification/approved-administrative-boundaries-20261024.json'));
if ($contents === false || ! hash_equals(self::HASH, hash('sha256', str_replace("\r\n", "\n", $contents)))) {
throw new RuntimeException('Approved administrative boundary manifest changed.');
}
$manifest = json_decode($contents, true, 512, JSON_THROW_ON_ERROR);
if (count($manifest['items']) !== 408) {
throw new RuntimeException('Exact 408-boundary scope required.');
}
$this->items = array_column($manifest['items'], null, 'id');
return $this->items;
}
public function allows(string $id, string $territoryId, string $geometryHash, string $propertiesHash): bool
{
$item = $this->snapshots()[$id] ?? null;
return $item !== null && $item['territory_id'] === $territoryId
&& hash_equals($item['geometry_sha256'], $geometryHash)
&& hash_equals($item['properties_sha256'], $propertiesHash);
}
}
+19 -4
View File
@@ -14,16 +14,31 @@ final class TerritoryGeometry
abort_unless($geography->references($management)->where('id', $id)->exists(), 404);
$path = $geography->path($id, $management);
$ids = array_map(fn ($node) => $node->id, $path);
$approved = app(ApprovedAdministrativeBoundaries::class);
$rows = DB::table('geo_geometries as g')->join('geo_locations as l', 'l.id', '=', 'g.geo_location_id')
->whereIn('g.geo_location_id', $ids)->whereNull('g.deleted_at')->whereNull('l.deleted_at')
->whereIn('g.data_classification', $management ? ['internal', 'public'] : ['public'])
->where(function ($query) use ($management, $approved) {
$query->where('g.data_classification', 'public');
$query->orWhere(function ($internal) use ($management, $approved) {
$internal->where('g.data_classification', 'internal');
if (! $management) {
$internal->whereIn('g.id', array_keys($approved->snapshots()));
}
});
})
->whereRaw("g.properties->>'purpose' = 'administrative_boundary'")
->whereIn('g.geometry_type', ['polygon', 'multipolygon'])
->whereRaw('ST_IsValid(g.geom) AND NOT ST_IsEmpty(g.geom) AND ST_SRID(g.geom)=4326')
->orderBy('g.geo_location_id')->orderBy('g.id')->limit(101)
->get(['g.id', 'g.geo_location_id', 'l.name', 'l.location_type', 'g.properties', 'g.data_classification',
DB::raw('ST_AsGeoJSON(ST_SimplifyPreserveTopology(g.geom,0.0001),6) as geometry')]);
$areas = $rows->take(100)->map(function ($row) use ($id) {
DB::raw('ST_AsGeoJSON(ST_SimplifyPreserveTopology(g.geom,0.0001),6) as geometry'),
DB::raw("encode(sha256(ST_AsEWKB(g.geom)),'hex') as geometry_sha256"),
DB::raw("encode(sha256(convert_to(g.properties::text,'UTF8')),'hex') as properties_sha256")]);
if (! $management) {
$rows = $rows->filter(fn ($row) => $row->data_classification === 'public'
|| $approved->allows($row->id, $row->geo_location_id, $row->geometry_sha256, $row->properties_sha256))->values();
}
$areas = $rows->take(100)->map(function ($row) use ($id, $management) {
$source = json_decode($row->properties, true, 512, JSON_THROW_ON_ERROR);
return ['id' => $row->id, 'territory_id' => $row->geo_location_id, 'location_type' => $row->location_type,
@@ -31,7 +46,7 @@ final class TerritoryGeometry
'title' => 'Limite administrativo: '.$row->name, 'purpose' => 'administrative_boundary',
'source_reference' => $source['authority'] ?? null, 'source_date' => $source['source_version'] ?? null,
'source_url' => $source['source_url'] ?? null, 'source_sha256' => $source['source_sha256'] ?? null,
'data_classification' => $row->data_classification, 'legal_currentness' => $source['legal_currentness'] ?? 'not_determined', 'url' => ''];
'data_classification' => $management ? $row->data_classification : 'public', 'legal_currentness' => $source['legal_currentness'] ?? 'not_determined', 'url' => ''];
})->all();
return ['territory_id' => $id, 'territory_registered' => true,
@@ -2001,5 +2001,379 @@
"qa_fixtures_copied": false,
"accepted_scenarios_repeated_manually": false,
"limitations": "No new scenario executed in homologation and no real dossier or personal data created. QA functional evidence reused; destination verification limited to revision, configuration and entry routes. Sector/fiscal exclusions remain explicit."
},
"closure_scope_reconciliation_20261024": {
"kind": "analysis_and_proposed_batch_only",
"reference_commit": "276d833a4b4f9071db328b653f48529b2dfac4f9",
"scope": [
"Financial planning consistency",
"Existing published IVA on net monthly sales",
"Data/calculation/save/recovery/report continuity",
"Screen/export values/context/source/version agreement",
"Explicit unavailable functionality without presumed results"
],
"accepted_proofs_reused": [
"Independent financial controls and cashflow recovery",
"Published IVA evaluator, server selection and frozen versions",
"Monthly sales reuse, zero versus missing, stale history",
"General CSV/Word/ZIP and native footer",
"Selected opportunity native PDF, 8036b9b0/cdf8fcfb"
],
"code_presence_reconciled": [
"3f5430c3 is ancestor of reference/stabilization-20261024",
"0b0cf761 is ancestor of reference/stabilization-20261024"
],
"public_availability_read": {
"url": "https://teste-investidor.tempo.co.mz/api/v1/fiscal-consumer",
"enabled": true,
"coverage_start": "2026-01-01",
"coverage_end": "2026-12-31",
"not_a_fresh_private_rule_audit": true
},
"items": [
{
"id": "S1",
"classification": "code_gap_new_isolated_diagnostic",
"consumer": "Saved cashflow to IVA fallback and report",
"cause": "Cashflow snapshot v1 has no original currency; consumers use current project currency",
"evidence": "outputs/simulator-scope-20261024/currency-gap.json",
"observed": "Saved amounts with original project USD become monthly IVA 123 MZN and MZN report after project currency changes; original currency absent from snapshot",
"proposal": "Version currency provenance; common mismatch/reconciliation guard; retain v1 bytes and require only missing currency where origin cannot be recovered",
"delta_proof": "Currency change, saved recovery and matching panel/IVA/report without conversion or loss"
},
{
"id": "S2",
"classification": "code_gap_directed_UI_proof_pending",
"consumer": "Monthly IVA pending execution recovery",
"cause": "Availability guard returns before pending-ID GET; button disabled when new admissions disabled",
"existing_server_contract": "Admitted execution/version and authenticated history remain readable",
"proposal": "Separate creation from read/resume by ID; do not reactivate consumer or create another execution",
"delta_proof": "Already admitted queued/running execution, new admissions unavailable, same ID recovered completed and reported without duplicate"
},
{
"id": "S3",
"classification": "specific_evidence_gap_not_presumed_defect",
"consumer": "Monthly IVA section in final dossier CSV/Word/native PDF",
"proposal": "One representative existing synthetic snapshot; compare exact values, zero/missing, selected context, source/version and current/history label; inspect only new monthly block pagination",
"reuse": "General exporters, ZIP, native footer and other accepted pagination proofs"
}
],
"proposed_order": [
"Fix S1 and S2 together",
"Check only changed dependencies",
"Complete S3 on resulting candidate, fixing only observed failures",
"Integrate and install through existing Git process after directed QA; verify installed delta"
],
"documentary_dependencies_identified": [],
"six_legacy_associations_and_two_incomplete_originals": "Remain documentary; no dependency identified for S1-S3",
"excluded": [
"Other taxes",
"New IVA rates/regimes/eligibility or incentives",
"Deductions/credits/refunds/final settlement",
"Full Modelo A",
"Extension beyond demonstrated 2026 coverage",
"Statistics and AI"
],
"functional_fixes_executed": false,
"deployment_executed": false,
"active_business_data_writes": 0,
"accepted_tests_reexecuted": false,
"historical_output_files_missing_in_this_workspace_do_not_revoke_acceptance": true,
"matrix_ids": [
"AUD-SIM-01",
"AUD-SIM-INT02",
"AUD-SIM-INT03",
"AUD-SIM-INT04",
"AUD-SIM-INT05",
"AUD-SIM-INT06",
"AUD-SIM-INT07",
"AUD-SIM-INT08"
]
},
"closure_qa_20261024": {
"status": "S1_S2_S3_passed_QA_ready_for_integration",
"authorization": "Tempo execute S1/S2 jointly in QA, then S3; no fiscal rule changes; candidate for existing Git integration",
"functional_candidate": "65b87819a0629f79f415788a6aa1a16fe031e37e",
"branch": "codex/simulator-closure-qa-20261024",
"preserved_reference": "276d833a4b4f9071db328b653f48529b2dfac4f9",
"integrated_main": false,
"homologation_deployed": false,
"S1": {
"status": "corrected_and_demonstrated_QA",
"snapshot_version": 2,
"currency_origin_preserved": true,
"original_USD_to_project_MZN": "guarded; report/panel retain USD, IVA refuses reuse as MZN",
"explicit_resolution": "Restore original currency on project; unknown legacy origin requires explicit currency confirmation only",
"legacy_v1_inputs_preserved": true,
"private_draft_roundtrip_passed": true
},
"S2": {
"status": "corrected_and_demonstrated_QA",
"method": "GET same admitted execution ID, credentials same-origin; wrong response ID rejected",
"new_admissions_disabled": true,
"visual_read_requests": 1,
"visual_create_requests": 0,
"real_API_owner_read_and_wrong_owner_denial": true,
"execution_count_preserved": true,
"rule_version_fingerprint_unchanged": true
},
"S3": {
"status": "final_CSV_Word_and_Edge_PDF_verified",
"artifact_inspection": {
"files": {
"monthly-final.csv": {
"bytes": 10639,
"sha256": "0b5fc670b0c252391aa99c0bff4e21af1e7a7b9c3a0e4ac6e2e2359a647d8a0e"
},
"monthly-final.docx": {
"bytes": 14101,
"sha256": "f83629e6ed3090a0d99d4843b11d4388048a6c993d2e1f7fabd87282bf0c2b38"
},
"monthly-final.html": {
"bytes": 11530,
"sha256": "09d40095550cae9bfdd29fcb5c6c7e0ad825a26170997a23347a8320a352b3a7"
}
},
"same_frozen_monthly_fields_verified": [
"160.0048",
"320.0096",
"1160.0348",
"2320.0696",
"MZN",
"opportunity:309",
"Manica",
"2026-01-01",
"2026-04-01",
"Fonte sintética QA",
"IVA-GERAL-BASE-LIQUIDA"
],
"csv_rows": 73,
"first_opportunity_not_included": true,
"fiscal_calculation_repeated": false,
"native_pdf_status": "awaiting_saved_file; request already sent"
},
"native_pdf_expected_path": "C:/laragon/www/portal-investidor/outputs/simulator-closure-qa-20261024/monthly-final.pdf",
"user_response": "Will save at requested path",
"HTML_is_not_native_pdf_proof": true,
"fixture": "Explicit synthetic QA, frozen accepted monthly numbers; not legal validation or real-rule publication",
"pdf_generation": {
"renderer": "Installed Microsoft Edge, isolated headless print engine",
"manual_print_dialog_used": false,
"source": "C:\\laragon\\www\\portal-investidor\\outputs\\simulator-closure-qa-20261024\\ui\\monthly-review.html",
"source_sha256": "b44ee5fc573f8deb8d5427f9f3b6a667c1d60fc23de4509d383b23acb9df2779",
"pdf": "C:\\laragon\\www\\portal-investidor\\outputs\\simulator-closure-qa-20261024\\monthly-final.pdf",
"bytes": 111910,
"sha256": "7719a05136f6dba668b621f551626060d10db5fcbb84815cdad112a1eab9079b",
"exit_code": 0
},
"pdf_inspection": {
"status": "native_file_read_and_rendered",
"exact_path": "C:\\laragon\\www\\portal-investidor\\outputs\\simulator-closure-qa-20261024\\monthly-final.pdf",
"sha256": "7719a05136f6dba668b621f551626060d10db5fcbb84815cdad112a1eab9079b",
"pages": [
{
"page": 1,
"size": [
594.9599609375,
841.9199829101562
],
"characters": 1633,
"image": "C:\\laragon\\www\\portal-investidor\\outputs\\simulator-closure-qa-20261024\\monthly-pdf-pages\\page-01.png",
"footer_boxes": [
[
48.0,
807.8525390625,
257.5710144042969,
817.9072265625
]
],
"footer_separated": true
},
{
"page": 2,
"size": [
594.9599609375,
841.9199829101562
],
"characters": 2299,
"image": "C:\\laragon\\www\\portal-investidor\\outputs\\simulator-closure-qa-20261024\\monthly-pdf-pages\\page-02.png",
"footer_boxes": [
[
48.0,
807.8525390625,
257.5710144042969,
817.9072265625
]
],
"footer_separated": true
},
{
"page": 3,
"size": [
594.9599609375,
841.9199829101562
],
"characters": 2047,
"image": "C:\\laragon\\www\\portal-investidor\\outputs\\simulator-closure-qa-20261024\\monthly-pdf-pages\\page-03.png",
"footer_boxes": [
[
48.0,
807.8525390625,
257.5710144042969,
817.9072265625
]
],
"footer_separated": true
},
{
"page": 4,
"size": [
594.9599609375,
841.9199829101562
],
"characters": 1832,
"image": "C:\\laragon\\www\\portal-investidor\\outputs\\simulator-closure-qa-20261024\\monthly-pdf-pages\\page-04.png",
"footer_boxes": [
[
48.0,
807.8525390625,
257.5710144042969,
817.9072265625
]
],
"footer_separated": true
}
],
"checks": {
"160.0048": true,
"320.0096": true,
"1160.0348": true,
"2320.0696": true,
"MZN": true,
"opportunity:309": true,
"Manica": true,
"2026-01-01": true,
"2026-04-01": true,
"Fonte sintética QA": true,
"IVA-GERAL-BASE-LIQUIDA": true,
"first_opportunity_absent": true,
"rule_version_4": true,
"zero_and_missing": true
},
"content_checks_passed": true,
"visual_review": "passed: all four rendered pages inspected; repeated headers, intact rows, no clipping/overlap, footers separated, numbering 1/4-4/4",
"regenerated": false,
"render_method": "Installed Microsoft Edge headless print engine, isolated profile; not manual print-dialog save",
"generation_authorized_by_user": true
},
"manual_print_dialog_used": false,
"regeneration_authorization": "User explicitly requested generating the file again to complete this step"
},
"checks": {
"frontend_new_cases": 4,
"frontend_total_direct_dependencies": 12,
"frontend_passed": 12,
"API_tests": 2,
"API_assertions": 23,
"QA_database": "simulator_fiscal_delta_20261021_test",
"QA_port": 55441,
"QA_transactions_rolled_back": true,
"typescript": "passed",
"build": "passed",
"diff_check": "passed"
},
"unchanged": [
"Fiscal server/evaluator/rules/sources",
"Schema/configuration/permissions/routes",
"Active business data",
"Accepted stabilization and opportunity-context closures"
],
"remaining": [],
"ready_for_integration": true,
"new_taxes_added": false,
"integration_note": "Candidate only; existing Git process and installed-delta verification still to execute after integration authorization; no homologation delivery claimed"
},
"closure_homologation_20261024": {
"status": "installed_delta_verified",
"authorization": "Tempo current conversation: integrate S1-S3 through existing Git process, install homologation and verify only delta",
"served_commit": "cd10cccbf45f53836d8bf7b2bd90794d99602cb9",
"functional_candidate": "65b87819",
"QA_closure_record": "cd10cccb",
"previous_main": "276d833a4b4f9071db328b653f48529b2dfac4f9",
"stabilization_tag": "reference/stabilization-20261024",
"Git_integration": "fast-forward after fetch; no conflicting/concurrent main changes found; no force",
"installed_delta": {
"served_revision": {
"commit": "cd10cccbf45f53836d8bf7b2bd90794d99602cb9",
"branch": "main",
"built_at": "2026-10-24T09:39:42.304316+00:00",
"manifest_sha256": "7957c776ab2c2eab516bcc6caedcdeb9943184645b4a21a89a4f709a4ba974f6"
},
"manifest_hash_matches": true,
"compiled_delta_markers": {
"currency_origin_guard": "assets/project-dossier-DNSA8_QR.js",
"original_currency_report": "assets/project-dossier-DNSA8_QR.js",
"same_id_recovery": "assets/services-BXcNtigY.js",
"explicit_currency_resolution": "assets/services-BXcNtigY.js"
},
"assets": [
{
"file": "assets/project-dossier-DNSA8_QR.js",
"status": 200,
"content_type": "application/javascript",
"sha256": "8cfe6743dddc5f35ddfe7ccdd6bb4d859719c1e263c6e51d9ac94b30a5fe1b76"
},
{
"file": "assets/simulation-form-ClLDsvMf.js",
"status": 200,
"content_type": "application/javascript",
"sha256": "fc829b447dcb1eec1cc550edcbfaa28f4db7b8973b4f86dab8a065cbbd8f0421"
},
{
"file": "assets/project-dossier-document-CJ_IVxDn.js",
"status": 200,
"content_type": "application/javascript",
"sha256": "ac4013924c4a78427c3e134654f331bf6fce313733532e7084ed85cf344106b2"
},
{
"file": "assets/services-BXcNtigY.js",
"status": 200,
"content_type": "application/javascript",
"sha256": "66d8af6670347a19e4006323f6c8b84e625a2dbaad6b4de777b4d979faa67093"
}
],
"simulator_public_status": 200,
"simulator_uses_installed_app_entry": "assets/app-Ds1F0RnS.js",
"existing_fiscal_availability": {
"data": {
"enabled": true,
"coverage_start": "2026-01-01",
"coverage_end": "2026-12-31",
"message": "IVA geral sobre base líquida disponível; enquadramento declarado."
}
},
"synthetic_dossiers_requests_or_calculations_created": 0,
"QA_acceptance_tests_repeated": false,
"method": "Read-only public installed delta: revision, manifest, compiled changed features, simulator page and existing availability; no authenticated DB audit claimed",
"shared_guard_asset_matches_QA_bytes": true,
"reference_276d833a_is_ancestor": true,
"fiscal_server_schema_config_routes_deploy_diff": [],
"cross_environment_asset_validation": "Served build checked against its own revision manifest and compiled changed features; not a claim of identical complete bundles across Windows QA and Linux production"
},
"functional_files": [
"resources/js/components/simulation-cashflow-planner.tsx",
"resources/js/components/simulation-monthly-iva.tsx",
"resources/js/lib/monthly-cashflow.ts",
"resources/js/lib/monthly-iva.ts",
"resources/js/lib/project-dossier.ts"
],
"preservation": "Stabilization ancestry and accepted proofs preserved; no fiscal server/schema/config/routes/deploy diff. No new authenticated database count audit claimed. No destination business-data mutation or new synthetic calculation requested.",
"accepted_QA_tests_repeated": false,
"PDF_regenerated_for_delivery": false,
"remaining_in_S1_S2_S3": [],
"legacy_snapshot_limit": "Original currency absent in v1 requires explicit confirmation; amounts preserved, MZN not presumed",
"not_full_all_tax_completion": true,
"next_fiscal_batch": "Scope from attached models; no automatic new tax/rule activation",
"statistics_and_AI": "remain deferred"
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,123 @@
{
"scope": "Shared administrative maps stabilization",
"source_revision": "cf2ddf21",
"candidate": "066707ec",
"owner_decision": "Tempo 2026-10-24: public consultation of the exact accepted administrative contours; originals private, no parcels or other internal layers",
"diagnosis": {
"data_loss": false,
"common_consumers": "Public-only filter excluded all 408 internal administrative boundaries",
"administration": "Legacy manage-portal gate excluded both approved superusers",
"comparison": "No change in common geometry service/controller/components between 30aed7cd and cf2ddf21",
"base_map": "Provider tiles and served assets respond; browser framing is checked after installation"
},
"qa": {
"database": "simulator_fiscal_delta_20261021_test",
"port": 55441,
"tests": 6,
"assertions": 64,
"result": "passed",
"phpstan": "passed",
"writes": "Only isolated QA transactions, rolled back"
},
"baseline": {
"published_fiches": 29,
"territorial_relations": 28,
"geometries": 408,
"geometry_rows_checksum": "7d8b71d3e65d89ffdc43c748b0572dbc"
},
"backup": {
"tables": 174,
"files_verified": 369,
"restore_tables_verified": 174,
"restore_network": "none",
"dump_sha256": "2fb5b4488799bb6072c38ee41ccf91fc2a55561363c19e280ab066363d9e0647"
},
"public_manifest_sha256": "06ab905fcf9b19115c3521bf81de03f37873880f7765f229b9e1f79d99df7adc",
"installed": {
"status": "installed",
"commit": "066707ece631f0a7c963031c211844fbdd68916b",
"release": "/var/www/investmoz-homologacao/releases/066707ece631f0a7c963031c211844fbdd68916b-6b7bb5bd",
"previous_release": "/var/www/investmoz-homologacao/releases/cf2ddf21a1457f4852fe6aa10069ec3b854f1567-7749f82e",
"database_migrations": 0,
"content_imports": 0
},
"deferred": [
"Statistics",
"AI"
],
"accepted_tests_repeated": false,
"public_projection": {
"available": 408,
"levels": {
"admin_post": 264,
"district": 133,
"province": 11
},
"failures": [],
"manifest_sha256": "06ab905fcf9b19115c3521bf81de03f37873880f7765f229b9e1f79d99df7adc"
},
"preservation": {
"tables": 174,
"changed_tables": {
"cache": {
"before": {
"rows": 104,
"checksum": "5d172b4d252c7b025f23ac70cf570cb6"
},
"after": {
"rows": 104,
"checksum": "a60f3e47e1ae28937404f05f45e33135"
}
},
"sessions": {
"before": {
"rows": 6,
"checksum": "dcdb7eba3f3e8f29f40ec3f7542be6ce"
},
"after": {
"rows": 7,
"checksum": "0059c0b0e16d532c6658fd420582f2e9"
}
}
},
"unchanged_tables": 172
},
"installed_file_sha256": {
"app/Services/ApprovedAdministrativeBoundaries.php": "401fd62795ca8f33106be6e07130d9207d6661695440bfc40f62f6f6b42bec64",
"app/Services/TerritoryGeometry.php": "00ce3e6dde92868b0d34b963b8e59a26e6663149cdc0398c645ef636ca382966",
"app/Http/Controllers/InvestmentTerritoryController.php": "3b184c6ea1b97f0abe86d01567d3d73444c49192c638478a6d53619e63d5d18d",
"app/Http/Middleware/EnsureScopedPortalAdministration.php": "1474db760f6bd6003a5325ae721698e90e2709a2354b51abde77f920a9dad81a",
"docs/investmoz/unification/approved-administrative-boundaries-20261024.json": "06ab905fcf9b19115c3521bf81de03f37873880f7765f229b9e1f79d99df7adc"
},
"pipeline_summaries": [
"OK (1 test, 3106 assertions)",
"built in 24.58s",
"OK (57 tests, 309 assertions)",
"# tests 216",
"# pass 216",
"# fail 0"
],
"proofs": {
"anonymous_http": {
"status": 200,
"territory_id": "dd219b89-cc17-5944-8c7d-d2766c82b526",
"selected": "DONDO",
"ancestor": "Sofala",
"areas": 2
},
"browser": {
"province_to_district": "passed",
"framing": "selected district with ancestor context",
"reload_preserves_selection": true,
"zoom": "passed",
"visible_tiles_loaded": 36,
"visible_tiles_failed": 0,
"screenshot": "outputs/map-stabilization-20261024/dondo-installed.png",
"screenshot_sha256": "7569f3b66aceb7ef9816c31bfaa172ebcdfa10e4911961b67a129a654b5cd349"
},
"business_rows_unchanged": true,
"active_synthetic_requests_created": 0
},
"status": "installed_delta_verified",
"remaining_in_scope": []
}
@@ -35,8 +35,9 @@
"number": 3,
"component": "Navegação contextual e manifestação de interesse",
"historic_status": "Disponível",
"current_preservation_status": "Navegação/percurso preservados; carteira documental reposta em 23/10: 29 dossiers, incluindo 15 projectos âncora.",
"evidence": "[Abrir](https://teste-investidor.tempo.co.mz/investimentos/iniciar) · Início, contexto e navegação abrem; gravação/recuperação aceites em QA reutilizadas. Sem dossier real no destino."
"current_preservation_status": "Continuity and multi-opportunity consumers delivered: 55136b44, 8036b9b0, cf2ddf21. Accepted proofs preserved.",
"evidence": "[Abrir](https://teste-investidor.tempo.co.mz/investimentos/iniciar) · Início, contexto e navegação abrem; gravação/recuperação aceites em QA reutilizadas. Sem dossier real no destino.",
"status_before_stabilization_closure": "Navegação/percurso preservados; carteira documental reposta em 23/10: 29 dossiers, incluindo 15 projectos âncora."
},
{
"number": 4,
@@ -175,8 +176,9 @@
"number": 23,
"component": "Cartografia e consumidores territoriais restantes",
"historic_status": "Não demonstrado",
"current_preservation_status": "Demarcação reposta: 408 geometrias. Polígonos de localidades e correspondências já pendentes não são nova regressão.",
"evidence": "[Abrir](https://teste-investidor.tempo.co.mz/oportunidades) · Catálogos/mapas abrem. 1 921 referências geográficas no alvo versus 1 990 locais; diferença não reconciliada por item fora do manifesto."
"current_preservation_status": "Shared maps delivered 066707ec/5d45541a: 408 accepted contours publicly available; originals private, 29 fiches and 28 relations preserved. Missing locality polygons remain future work.",
"evidence": "[Abrir](https://teste-investidor.tempo.co.mz/oportunidades) · Catálogos/mapas abrem. 1 921 referências geográficas no alvo versus 1 990 locais; diferença não reconciliada por item fora do manifesto.",
"status_before_stabilization_closure": "Demarcação reposta: 408 geometrias. Polígonos de localidades e correspondências já pendentes não são nova regressão."
},
{
"number": 24,
@@ -210,8 +212,9 @@
"number": 28,
"component": "Pesquisa — estados do recorte R2",
"historic_status": "Disponível",
"current_preservation_status": "Pesquisa e selectores corrigidos em 22/10; a carteira vazia é tratada como perda separada.",
"evidence": "[Abrir](https://teste-investidor.tempo.co.mz/oportunidades) · Catálogos abrem; quatro estados e protecção contra respostas antigas preservados do R2, sem repetir ensaios aceites."
"current_preservation_status": "Accepted search/selectors preserved; portfolio visibility and anchor continuity restored and delivered cf2ddf21. No current portfolio-empty fault.",
"evidence": "[Abrir](https://teste-investidor.tempo.co.mz/oportunidades) · Catálogos abrem; quatro estados e protecção contra respostas antigas preservados do R2, sem repetir ensaios aceites.",
"status_before_stabilization_closure": "Pesquisa e selectores corrigidos em 22/10; a carteira vazia é tratada como perda separada."
},
{
"number": 29,
@@ -945,5 +948,79 @@
"new_roles_or_approvers": false,
"new_content_approval": false
},
"qa_recovery_preparation_note": "An initial QA audit-key collision rolled back atomically. Audit identity was scoped per original before the successful QA and destination recovery; no active attempt used the faulty key."
"qa_recovery_preparation_note": "An initial QA audit-key collision rolled back atomically. Audit identity was scoped per original before the successful QA and destination recovery; no active attempt used the faulty key.",
"stabilization_closure_20261024": {
"basis": "Tempo explicit sequence and technical preservation responsibility, 24/10/2026",
"installed_functional_baseline": "066707ece631f0a7c963031c211844fbdd68916b",
"map_receipt": "5d45541a",
"known_open_faults_in_delimited_stabilization_scope": [],
"does_not_close_all_43_components": true,
"prior_losses_do_not_share_a_proven_single_cause": true,
"accepted_closures": [
"6b544548/4098192a",
"d8224ba8/af926180",
"55136b44/b6a626c9",
"8036b9b0/cdf8fcfb",
"cf2ddf21/c1f4a719",
"066707ec/5d45541a"
],
"documentary_limits": [
"Six legacy opportunity/anchor-cluster pairs without documented identity remain indeterminate: AUD-REL-02",
"Two incomplete legacy originals retain their existing documentary limit"
],
"not_yet_proven_component_numbers": [
6,
18,
21,
22,
24,
25,
35
],
"future_requirement_ids": [
"AUD-MARKET-01",
"AUD-ESTABLISHMENT-01",
"AUD-FUND-01",
"AUD-COMPANY-01",
"AUD-COMPANY-02",
"AUD-CAPITAL-01",
"AUD-CAPITAL-02",
"AUD-FINTECH-01"
],
"territorial_count_reconciliation": {
"source": "outputs/preservation-inventory-20261023/local.json and homologation.json",
"public_active_local": 1921,
"public_active_target": 1921,
"local_archived_internal_groups": [
32,
32,
5
],
"individual_identity_comparison_of_archived_69_claimed": false,
"reactivation_authorized": false
},
"new_control": {
"test": "tests/Unit/ApprovedAdministrativeBoundariesTest.php",
"automatic_delivery_suite": "Unit",
"tests": 2,
"assertions": 8,
"result": "passed",
"scope": "Approved public snapshot identity; changed geometry/metadata, wrong territory and new internal identity rejected",
"http_access_contract": "tests/Feature/TerritoryGeometryConsumerAccessTest.php",
"http_contract_run_when": "Geometry/access/hierarchy changes, on isolated QA only; existing accepted result reused here"
},
"technical_preservation_owner": "Codex and programmers",
"Tempo_review": "Utility, clarity and presentation; no obligation to rediscover technical regressions",
"sequence": [
"Corrections and stabilization",
"Fiscal simulator with defined scope",
"Real estate",
"Consumer goods and franchises",
"Statistics and dashboards",
"AI"
],
"accepted_acceptance_tests_repeated": false,
"active_database_writes": 0,
"runtime_application_source_changes": 0
}
}
@@ -0,0 +1,80 @@
{
"reference_commit": "276d833a4b4f9071db328b653f48529b2dfac4f9",
"runtime_files_changed_since_066707ec": [],
"matrix_records": 3423,
"matrix_columns": 49,
"updated_existing_rows": [
"AUD-GEO-01",
"AUD-GEO-02",
"AUD-GEO-03",
"AUD-REL-01",
"AUD-REL-02",
"AUD-DESK-01",
"AUD-DESK-02",
"AUD-SIM-01",
"AUD-LEG-06"
],
"accepted_receipts_unchanged_sha256": {
"map-stabilization-receipt-20261024.json": "2b69216b644f2244897431a10f3e6828e822d1317fab326a457a5934261c1618",
"anchor-integration-receipt-20261023.json": "86b0d01aa2fb03be84c1b1c6254ea0932abe813375f00560aaf39ce78d199f3d",
"treatment-scope-installation-receipt-20261023.json": "a0904250e1ed201276fce0318b9cc131edf4c8ec0ab6d449536b1e1344906798",
"multi-opportunity-context-installation-receipt-20261023.json": "3e1030de9aafd916252926a33d18cf808a05e389ddce5d9a3cd5230f039bc8f2"
},
"new_tests": {
"tests": 2,
"assertions": 8,
"passed": true
},
"active_database_writes": 0,
"status": "installed_reference_verified",
"date_Africa_Johannesburg": "2026-10-24",
"git_tag": "reference/stabilization-20261024",
"served_revision": {
"commit": "276d833a4b4f9071db328b653f48529b2dfac4f9",
"branch": "main",
"built_at": "2026-10-24T09:10:08.359035+00:00",
"manifest_sha256": "949d1047867f5879e724e5ddc38d4865b06aeb92641337d3952ea7b5ad55bb7a"
},
"previous_functional_revision": "066707ece631f0a7c963031c211844fbdd68916b",
"verification_method": "New unit control tested; semantic matrix/inventory reconciliation; canonical accepted receipt hashes; empty Git diff for runtime/schema/config/permissions/deploy; public installed revision. Prior accepted technical and visual proofs reused.",
"accepted_tests_repeated_by_this_review": false,
"automatic_installer": "Normal existing pipeline; no extra manual acceptance round",
"fresh_authenticated_database_read_claimed": false,
"business_data_preservation": "Accepted map receipt 5d45541a reused: 29 fiches, 28 links, 408 geometries, 172 business/nonvolatile tables equal; no runtime or data mutation introduced by this reference delta",
"known_open_stabilization_faults": [],
"documentary_pending": [
"AUD-REL-02: six legacy opportunity/anchor-cluster pairs remain indeterminate without documented relationships",
"Two incomplete originals in the legacy collection retain the accepted documentary limit"
],
"not_yet_proven_existing_components": [
6,
18,
21,
22,
24,
25,
35
],
"future_requirements": [
"AUD-MARKET-01",
"AUD-ESTABLISHMENT-01",
"AUD-FUND-01",
"AUD-COMPANY-01",
"AUD-COMPANY-02",
"AUD-CAPITAL-01",
"AUD-CAPITAL-02",
"AUD-FINTECH-01"
],
"technical_preservation_owner": "Codex and programmers",
"Tempo_evaluation": "Usefulness, clarity, presentation; does not need to rediscover technical regressions",
"next": "Fiscal simulator scope definition; not started by this consolidation",
"reference_sequence": [
"Stabilization",
"Defined fiscal simulator scope",
"Real estate",
"Consumer goods and franchises",
"Statistics and dashboards",
"AI"
],
"not_complete_portal_acceptance": true
}
@@ -1,5 +1,99 @@
# Rastreabilidade, utilização e formação
## Simulador S1–S3 — entregue na homologação em `cd10cccb`, 24/10/2026
Por autorização expressa da Tempo após o fecho QA, integrado pelo Git existente, de `276d833a` para **`cd10cccbf45f53836d8bf7b2bd90794d99602cb9`**, que contém a correcção funcional `65b87819`. `git fetch` não encontrou alterações concorrentes em main; avanço normal, sem force. A referência `276d833a`/`reference/stabilization-20261024` permanece ancestral. Não houve edição da pasta servida nem migração/importação solicitada.
**Delta instalado conferido por leitura pública:** `/build/revision.json` identifica `cd10cccb`; SHA256 do manifesto servido coincide com o da revisão (`7957c776ab2c2eab516bcc6caedcdeb9943184645b4a21a89a4f709a4ba974f6`). Quatro assets do simulador/relatório responderam 200; os bundles contêm a guarda da moeda original, a confirmação explícita e a recuperação por mesmo ID. O asset comum da guarda de moeda coincide por bytes com o build QA. O simulador responde 200 e referencia a entrada app do manifesto instalado. O consumidor IVA mantém disponibilidade `enabled=true` e cobertura de 2026. Não se exige identidade dos bundles completos entre Windows QA e Linux; a conferência usa o manifesto do próprio build servido, os assets e o delta de código.
O diff de `app`, `database`, `config`, `routes` e `deploy` contra `276d833a` é vazio: sem alteração de motor, regras/fontes fiscais, schema, permissões, configuração ou processo de entrega. Cinco fontes frontend constituem o delta funcional de moeda/recuperação/relatório. Não foram criados dossiers, pedidos ou cálculos sintéticos na homologação, nem repetidos os ensaios aceites. Provas QA 12 frontend, 2 API/23 asserções, TypeScript/build e PDF de quatro páginas são reutilizadas, sem nova impressão. Não se apresenta esta leitura pública como auditoria autenticada nova dos dados protegidos.
**Saldo S1–S3: entregue, sem pendências deste lote.** Nos snapshots antigos sem moeda, a confirmação explícita da origem continua necessária, preservando os montantes; não se presume MZN nem se convertem valores. Este fecho abrange moeda da tesouraria, recuperação do IVA mensal e exportações; **não conclui todos os impostos**. Os modelos anexados delimitam o próximo lote fiscal, sem activação implícita. As pendências documentais anteriores mantêm-se no seu âmbito. Recibo integrado existente: chave `closure_homologation_20261024`; prova do delta em `outputs/simulator-delivery-20261024/installed-delta.json`. Os estados QA/pendentes abaixo são histórico superado por esta entrega.
## Simulador S1/S2/S3 — candidata QA `65b87819`, 24/10/2026
**Actualização de fecho S3:** a Tempo pediu expressamente voltar a gerar o ficheiro. O PDF foi gravado directamente em `outputs/simulator-closure-qa-20261024/monthly-final.pdf` pelo motor de impressão do Microsoft Edge em modo headless, com perfil isolado, a partir da mesma saída HTML final; não foi gravação manual pelo diálogo de impressão. Ficheiro de 111 910 bytes, SHA256 `7719a05136f6dba668b621f551626060d10db5fcbb84815cdad112a1eab9079b`. As **quatro páginas A4** foram extraídas, renderizadas e inspeccionadas visualmente: valores exactos, MZN, oportunidade `309`/Manica, Janeiro–Abril/2026, fonte sintética QA e versão 4 conservados; zero distinto de falta, cabeçalhos repetidos, linhas intactas, rodapés separados e numeração 1/4–4/4. CSV e Word previamente conferidos mantidos, sem recálculo fiscal. **S1, S2 e S3 concluídos em QA; candidata pronta para integração pelo Git.** Saldo QA deste lote: zero; integração, disponibilização e conferência do delta instalado ainda não executadas. Os estados de espera abaixo são histórico anterior a esta geração/conferência e não reabrem S3. Recibo integrado e mesmas oito linhas da matriz actualizados; referência `276d833a` e regras fiscais preservadas.
Execução autorizada pela Tempo após `be62783b`, preservando `276d833a` e a etiqueta `reference/stabilization-20261024`. Ramo `codex/simulator-closure-qa-20261024`; **não integrado em main nem entregue na homologação**. Cinco fontes frontend alteradas; sem alterações em motor/regras/fontes fiscais, schema, configuração, permissões, rotas ou dados activos. Sem impostos adicionais.
| Recorte | Correcção e resultado dirigido |
| --- | --- |
| **S1 — corrigido e demonstrado em QA** | Novos snapshots de tesouraria v2 conservam moeda de origem. Painel, CSV próprio e relatório usam essa moeda; o fallback de vendas para IVA recusa divergência. USD → MZN conserva `123 USD`, explica a diferença e permite restaurar USD no projecto por acção explícita. Os montantes não são convertidos nem reinterpretados. Snapshots v1 continuam legíveis; moeda sem origem recuperável exige confirmação única, conservando valores/modo/meses vazios. Não se presume MZN. A observação visual detectou um rótulo residual de saldo na moeda actual; corrigido para a moeda de origem. O rótulo do selector passou a «Moeda do projecto», distinguindo-o dos pressupostos guardados. |
| **S2 — corrigido e demonstrado em QA** | Recuperação mensal usa GET no mesmo ID, com credenciais e verificação do identificador devolvido; não depende da disponibilidade para novas admissões. Botão específico continua disponível com consumidor desligado. Resposta não substitui snapshot alterado entretanto ou componente desmontado. Interface isolada: consumidor indisponível, uma consulta GET, zero criações POST, mesma execução concluída. API real de QA: execução admitida → consumidor desligado só na configuração do teste → mesmo ID em fila/concluído; outra conta continua recusada e contagem de execuções não aumenta. Regras existentes reutilizadas, sem criar/publicar/suspender regra; fingerprint integral das versões igual. |
| **S3 — CSV/Word e fonte de impressão conferidos; PDF nativo ainda pendente** | Mesmo bloco mensal sintético congelado, sem recalcular: Janeiro `160.0048`, Fevereiro `320.0096`, Março zero, Abril sem informação; MZN, oportunidade `309`/Manica, período Janeiro–Abril/2026, fonte identificada como sintética QA e versão 4. CSV e Word finais lidos e comparados, incluindo ausência da primeira oportunidade/Sofala. A fonte HTML conserva os mesmos dados. O relatório está aberto no Edge e a Tempo indicou que guardará o PDF em `outputs/simulator-closure-qa-20261024/monthly-final.pdf`; falta ler e renderizar esse ficheiro para conferir apenas a paginação do bloco. HTML não substitui essa prova. |
Provas: quatro testes novos em `simulator-closure-scope.test.mjs`, mais oito casos das dependências directamente afectadas, **12/12 aprovados**; `SimulatorClosureRecoveryTest`, **2 casos/23 asserções**, transacções revertidas no clone nominal `simulator_fiscal_delta_20261021_test:55441`. TypeScript e build aprovados. As suites de aceitação não afectadas, ZIP, cálculos fiscais, PDFs/rodapé anteriores e contextos por oportunidade são reutilizados; não se exigiu à Tempo repetir essas provas. O ensaio visual usa componentes reais com transporte simulado explicitamente identificado, complementado pelo teste API de permissões; não é apresentado como servidor real ou processo na homologação.
Artefactos novos: `outputs/simulator-closure-qa-20261024/` — CSV/Word/HTML finais, snapshot congelado, inspecção/hash dos ficheiros, captura de recuperação e resultados dos testes. O estado executado fica no recibo integrado existente, chave `closure_qa_20261024`, e nas mesmas oito linhas da matriz. **Limite corrente:** S1/S2 em QA; S3 aguarda somente o PDF nativo guardado. A candidata não é declarada pronta para integração enquanto faltar essa conferência. Os seis pares legados e os dois originais incompletos não têm dependência efectiva neste lote.
## Simulador — âmbito de fecho reconciliado e lote proposto, 24/10/2026
Pedido expresso da Tempo após a referência `276d833a`/`reference/stabilization-20261024`, confirmada segundo o relatório, sem lhe atribuir leitura do recibo local. Esta passagem é **análise e proposta**, sem correcção funcional, integração, nova publicação/activação fiscal ou alteração de dados. A ordem de referência mantém-se. Não se acrescentam impostos nem se reabre o lote de estabilização.
### O que já existe e permanece aceite
| Frente | Implementação e provas reutilizadas |
| --- | --- |
| Planeamento financeiro | Plano empresarial por produto/mês, tesouraria, pressupostos editáveis, moeda e cenários de planeamento; controlos independentes anteriores de saldos positivo/negativo/nulo/incompleto, crédito e projecções. `business-financial-plan`, `monthly-cashflow` e recibos integrados de 20–22/10. Exemplos demonstrativos e imposto manual continuam identificados como planeamento, sem serem taxas legais ou valores do investidor. |
| IVA implementado | Consumidor publicado, selecção e versão congeladas no servidor, avaliador existente; vendas mensais do plano ou receita constante já guardada, indicação explícita de base líquida, zero distinto de falta e IVA separado da caixa. `SimulationPublishedFiscalTest`, quatro casos de `monthly-iva.test.mjs` e ensaio Edge mensal anteriores reutilizados. `3f5430c3` e `0b0cf761` pertencem à referência instalada; as notas históricas «só QA» não significam hoje ausência deste código. A disponibilidade pública consultada nesta passagem devolveu `enabled=true`, cobertura 2026. Isso não é uma nova auditoria dos parâmetros protegidos; o cadastro/publicação real v1 mantém o recibo de `d3fe8a02`. |
| Continuidade | Dois dossiers/nova sessão, isolamento, recuperação, erro e nova tentativa, históricos e hashes já provados. A escolha da oportunidade e o PDF correspondente estão entregues em `8036b9b0`/`cdf8fcfb`; não repetir. Inputs, rascunho e snapshots usam os mecanismos existentes, sem voltar a preencher vendas no IVA. |
| Resultados/exportações | Resumo/detalhe, CSV/Word, erro e nova tentativa de exportação, ZIP e rodapé do PDF nativo têm provas aceites. O rodapé nativo de tesouraria foi corrigido e conferido em 21/10; o PDF guardado por oportunidade foi conferido em 23/10. O bloco mensal usa o mesmo snapshot fiscal e não recalcula imposto ao exportar. A prova específica restante desse bloco consta de S3 abaixo; não reabrir os restantes exportadores ou PDFs. |
| Indisponibilidade | Fora da cobertura demonstrada de 2026, moeda diferente de MZN, preços com IVA incluído/base desconhecida, enquadramento não declarado e vendas em falta permanecem explícitos ou indeterminados, sem substituir por zero. Deduções, créditos, reembolsos, regimes especiais, apuramento final/Modelo A completo e outros impostos não são implementados por este lote. Cobertura técnica não é uma data legal de revogação. |
Reutilizados os recibos versionados `integrated-review-continuation-20261020-receipt.json`, `treatment-scope-installation-receipt-20261023.json` e os fechos posteriores do manual. Alguns caminhos históricos `outputs/` não existem nesta cópia do workspace: essa ausência não revoga provas aceites nem autoriza regenerá-las. Nenhuma suite de aceitação anterior foi reexecutada nesta análise.
### Saldo concreto — duas lacunas de código e uma prova específica
| ID / situação | Ecrã, causa e efeito delimitado | Lote proposto e critério de fecho |
| --- | --- | --- |
| **S1 — lacuna de código confirmada por diagnóstico isolado novo** | **Tesouraria → vendas reutilizadas pelo IVA → relatório.** `CashflowDraftSnapshot` v1 guarda montantes/modo, mas não a moeda de origem. O painel e o relatório usam a moeda actual do projecto; o fallback `monthlySalesFromDraft` não tem moeda guardada para comparar. Serializar receita `123` enquanto o projecto está em USD e depois mudar o projecto para MZN produz uma entrada IVA `123 MZN` e relatório MZN, sem conservar USD. O plano empresarial já tem protecção própria de moeda; a lacuna é a tesouraria guardada, não todos os planeadores. | Versionar a moeda dos pressupostos de tesouraria e partilhar a guarda entre painel, IVA e relatório. Divergência exige reconciliação explícita, sem conversão/reclassificação silenciosa nem perda dos montantes. Snapshots v1 continuam legíveis e intactos; moeda sem origem recuperável fica por confirmar, nunca presumida como MZN. Prova nova só deste delta: USD → MZN, gravação/recuperação, ecrã/IVA/exportação coerentes; valores e edições conservados. Diagnóstico sem DB/rede em `outputs/simulator-scope-20261024/currency-gap.json`. |
| **S2 — lacuna identificada no código; prova de interface dirigida a fazer com a correcção** | **IVA mensal, execução já admitida ainda em fila/processamento.** Em `SimulationMonthlyIva`, o efeito regressa por `!available` antes de consultar o ID pendente e o botão também fica desactivado. A suspensão do consumidor, que deve impedir novas admissões, bloqueia assim a recuperação pela componente mensal. O servidor já preserva a versão admitida e permite consultar o histórico; não há falha de cálculo ou perda da execução demonstrada. | Separar nova admissão de consulta/retoma por ID. Execução admitida deve ser recuperável com consumidor suspenso, respeitando autenticação/dono, sem novo POST nem nova regra. Prova dirigida em QA: mesma execução pendente → indisponibilidade para novas admissões → conclusão/recuperação por mesmo ID → rascunho/relatório com o snapshot correcto e sem duplicação. Não repetir os testes aceites do avaliador ou publicar/suspender a regra real de homologação. |
| **S3 — ainda sem prova específica, não defeito presumido** | **Bloco mensal de IVA nos ficheiros finais.** Há prova de reutilização das vendas, snapshots mensais no servidor e composição do relatório, e provas aceites dos exportadores gerais. Não há neste inventário uma conferência explícita de ficheiros finais com o bloco mensal completo — valores exactos, estado actual/histórico, oportunidade, fonte e versão — incluindo a paginação desse bloco. Os PDFs anteriores cobriam outros deltas. | Conferir um cenário mensal representativo por ecrã e CSV/Word, e apenas a paginação do bloco mensal no PDF nativo final. Reutilizar execução/snapshot sintético aceite, se recuperável, sem recalcular para testar exportação. Comparar montantes exactos, zero/falta, contexto, fonte, versão e aviso de ausência de apuramento final. ZIP e restante paginação/rodapé reutilizam as provas aceites. Corrigir apenas divergência efectivamente observada; não presumir falha de exportador. |
**Proposta de execução:** fechar S1 e S2 juntos, pelos consumidores de pressupostos/snapshots; verificar apenas os seus deltas e executar S3 sobre a candidata resultante. Integrar e disponibilizar somente depois dessas provas, pelo Git existente e preservando alterações concorrentes; conferir a revisão e o delta instalado. Não migrar dados activos nem publicar outra regra por esta proposta. As guardas, mensagens e compatibilidade de snapshots devem ser tratadas no mesmo lote, sem criar motor, formulários ou aprovações novos.
**Dependências reais:** nenhuma identificada entre S1–S3 e as seis associações legadas ou os dois originais incompletos. As fontes da regra IVA já cadastrada e os recibos fiscais existentes são reutilizados. Declaração Modelo A completa, outros impostos, elegibilidade/incentivos novos e extensão temporal não entram neste fecho. O resultado esperado é o ciclo **dados → cálculo disponível → gravação → recuperação → relatório** coerente neste âmbito, não a certificação de um simulador fiscal integral. A proposta está registada nas linhas existentes `AUD-SIM-01` e `AUD-SIM-INT02`–`08`; as notas e estimativas históricas ficam subordinadas a este saldo de três itens, sem outro backlog.
## Fecho do lote de estabilização e sequência de referência — 24/10/2026
**Ordem expressa da Tempo:** (1) correcções e estabilização; (2) simulador fiscal com âmbito definido; (3) Imobiliário; (4) Bens de consumo e franquias; (5) Estatística e painéis; (6) IA. A preservação técnica é responsabilidade do Codex e dos programadores. A Tempo avalia utilidade, clareza e apresentação; não tem de redescobrir regressões para comprovar preservação.
Consolidação sobre a revisão instalada `066707ec`, recibo `5d45541a`, mantendo a mesma matriz e o inventário dos 43 componentes. Nesta passagem não foi encontrada nova falha executável nos recortes de estabilização já delimitados. A revisão de código confirma que a primeira oportunidade usada na migração do contexto comum não substitui o âmbito dos consumidores: `InvestmentConsumerScope` filtra a escolha antes do snapshot e dos relatórios. Não foram repetidas as provas aceites, impressões, submissões ou alterações de dados. A confirmação da Tempo anterior baseou-se no relatório comunicado, sem conferência do recibo local; não é registada como uma nova prova técnica ou visual.
| Situação no lote | Percursos e saldo concreto |
| --- | --- |
| Resolvido e entregue | Gravação/idioma, proveniência do relatório e Beira/sete oportunidades: `6b544548`/`4098192a`. Complementos: `d8224ba8`/`af926180`. Zonas/parques, Terra/DUAT, Balcão e manifestação por oportunidade: `55136b44`/`b6a626c9`. Selector do simulador, etapas/requisitos e pedido Terra correcto, incluindo PDF guardado: `8036b9b0`/`cdf8fcfb`. Âncoras, sector/localização e continuidade: `cf2ddf21`/`c1f4a719`. Mapas: `066707ec`/`5d45541a`. Pendências históricas destes trechos ficam superadas pelas respectivas entregas. |
| Falha conhecida ainda aberta neste lote | **Nenhuma identificada nos recortes enumerados.** Não equivale a aceitação integral dos 43 componentes ou de todas as variantes do portal. Não se inventa uma nova correcção para justificar outra ronda. |
| Pendência documental preservada | `AUD-REL-02`: os pares das seis oportunidades legadas com âncoras/clusters continuam indeterminados quando não há relação documentada. Não confundir com as 28 relações recuperadas dos projectos. Preservar a preparação independente; não associar por proximidade ou nome. Dois originais incompletos do acervo legado conservam o limite documental já registado, sem representar perda causada pelos mapas. |
| Ainda sem prova no âmbito próprio | Inventário existente: consolidação de duplicados distintos (componente 6); aplicação/editor e versões de checklists (18); partilha e variantes de sala (21/35); modalidades/SLA e fecho operacional/documental (22/25); acessibilidade além das provas aceites (24). Não foram identificadas novas regressões nesses itens; não os declarar concluídos nem exigir processos reais/sintéticos na homologação para fechar este lote. Mantêm os critérios próprios da matriz, sem atribuição de novo prazo ou prioridade. |
| Desenvolvimento futuro, separado de regressões | Negócios similares por raio (`AUD-MARKET-01`), importador de empresas/empreendimentos (`AUD-ESTABLISHMENT-01`), catálogo de fundos (`AUD-FUND-01`), comparador societário e modelos específicos (`AUD-COMPANY-01/02`), guiões de capitalização/Bolsa (`AUD-CAPITAL-01/02`), polígonos próprios ainda inexistentes e enquadramento Fintech específico. Variantes de Imobiliário/franquias, Estatística/painéis e IA seguem a ordem acima; SuiteCRM mantém a opção anterior de desactivação. Não se declara que todo o código destes módulos esteja ausente. |
**Causas independentes:** os mapas tinham filtro público e dependência de uma permissão antiga; a carteira sofreu perda de visibilidade de registos presentes; as relações de âncoras tinham omissão no transporte entre identidades documentais. As provas delimitam esses casos e não estabelecem a causa de todas as perdas anteriores. O inventário histórico territorial tinha 1 990 linhas locais contra 1 921 no destino: **1 921 públicas activas em ambos, mais 69 internas arquivadas apenas no local**, repartidas em 32+32+5. Isto explica a diferença agregada; não é prova de correspondência individual das 69 nem autoriza reactivá-las. Nenhuma reposição adicional decorre dessa contagem.
### Controlo técnico nas próximas entregas
Reutilizar o inventário/recibos e comparar apenas o delta e as dependências afectadas. O pipeline existente conserva detecção de alterações concorrentes na árvore instalada, contrato de schema, QA isolado, assets imutáveis, troca atómica e reversão por falha de saúde. A suite normal de entrega passa também a incluir `ApprovedAdministrativeBoundariesTest`: identidade aceite e recusa de novo contorno interno, território trocado, geometria ou metadados alterados. É controlo novo de âmbito público, não repetição da demonstração visual aceite. As duas provas novas passaram, com oito asserções e sem acesso à base.
Quando se alterar acesso, hierarquia ou serviço de mapas, executar o delta correspondente de `TerritoryGeometryConsumerAccessTest` em QA isolado; cobre o filtro público e os perfis aprovados sem `manage-portal`, incluindo MFA administrativo. O teste unitário isolado não substitui esse contrato HTTP. Para alterações de contexto, usar os casos afectados de `InvestmentTreatmentScopeTest`, `InvestmentAnchorIntegrationTest` e consumidores/complementos, sem repetir indiscriminadamente toda a suite de aceitação. No destino, comparar identidades/fontes/estado de publicação das 29 fichas, 28 relações e 408 geometrias com os manifestos aceites, configuração e permissões equivalentes; mudanças legítimas posteriores são reconciliadas explicitamente, não sobrescritas. Backup/restauro e acção autenticada precedem qualquer escrita de dados activos.
**Saldo de fecho:** zero falhas técnicas conhecidas abertas neste lote; limites documentais, itens sem prova e desenvolvimento futuro permanecem identificados acima e nas mesmas linhas da matriz. Não se declara a aplicação transversal integralmente concluída. O próximo lote é a delimitação do simulador fiscal; esta passagem não activa cálculos, fontes, estatísticas ou IA. A avaliação de navegação/apresentação da Tempo pode continuar pelo percurso normal e alimenta apenas falhas concretamente observadas.
**Referência consolidada instalada: `276d833a4b4f9071db328b653f48529b2dfac4f9`**, confirmada pelo `/build/revision.json` público. Etiqueta Git `reference/stabilization-20261024`. A comparação Git de `app`, `resources`, `routes`, `database`, `config`, `public` e `deploy` com `066707ec` é vazia; não há novo delta funcional a reensaiar. Os recibos aceites conservam os mesmos bytes canónicos. Mantidas 3 423 linhas lógicas e 49 colunas da matriz; só nove linhas existentes foram reconciliadas. A prova anterior de preservação das 29 fichas, 28 relações e 408 geometrias é reutilizada, não apresentada como nova consulta autenticada da base nesta passagem. [Recibo da referência](stabilization-reference-receipt-20261024.json).
## Mapas — estabilização comum e divulgação delimitada, 24/10/2026
Diagnóstico por leitura autenticada na homologação `cf2ddf21`: **29 fichas, 28 relações territoriais e 408 geometrias válidas**, sem eliminação. Os 408 limites INE/2019 conservam classificação interna, propósito administrativo e SRID 4326. O fingerprint integral das linhas é `7d8b71d3e65d89ffdc43c748b0572dbc`. Não há RLS nas tabelas comparadas. O serviço comum não mudou desde `30aed7cd`: os consumidores gerais pediam exclusivamente geometrias públicas; o cadastro usava ainda `manage-portal`, ausente nos dois superutilizadores aprovados. A prova anterior demonstrava o cadastro privado, não a divulgação pública. O mapa base, tiles e assets carregam; a resposta territorial vazia explica a ausência de contornos e de enquadramento do território escolhido.
**Decisão expressa da Tempo nesta passagem:** «Sim, disponibilizar os contornos administrativos aceites». Implementada uma projecção pública de leitura dos [408 snapshots exactos](approved-administrative-boundaries-20261024.json), conferindo identidade e hashes da geometria original e metadados. Não há reimportação, reclassificação de linhas, publicação de parcelas, acesso aos ficheiros originais ou abertura de outras camadas internas. Alterações posteriores não recebem divulgação automática. Os perfis autorizados reutilizam o acesso privado já aprovado; a leitura administrativa conserva MFA. Respostas privadas não são guardadas em cache partilhada.
QA isolado `simulator_fiscal_delta_20261021_test:55441`: seis testes dirigidos, 64 asserções; selecção, gravação/recuperação no dossier e prefill do simulador com a mesma identidade, leitura pública autorizada, bloqueio de camadas não autorizadas e de snapshots alterados, permissões/MFA e fingerprint original preservado. PHPStan dos quatro ficheiros funcionais passou. Backup fresco: 174 tabelas, 369 ficheiros conferidos; restauro isolado sem rede reproduziu as 174 tabelas.
**Entregue na homologação em `066707ece631f0a7c963031c211844fbdd68916b`**, por avanço normal do Git, preservando `cf2ddf21` e as alterações concorrentes. Instalador: contrato 1/3106, PHP 57/309, frontend 216/216 e build aprovados; zero migrações e zero importações. Os cinco ficheiros funcionais instalados coincidem com o Git. Consulta pública pelo serviço instalado: **408/408 disponíveis**, 11 províncias, 133 distritos e 264 postos, zero falhas. HTTP anónimo recupera Dondo e Sofala pela identidade canónica, com Dondo seleccionado e Sofala como contexto ancestral. Conferência visual: mudança de província para distrito actualiza o contorno e enquadramento; escolha preservada após recarregar, zoom operacional e 36 tiles carregados sem falha na observação final. Houve falhas transitórias de tiles na janela anterior; os mesmos URLs responderam 200 e o recarregamento recuperou a base, sem alteração de configuração ou fornecedor.
Comparação das 174 tabelas: **172 iguais por contagem/hash**, diferenças apenas em `cache` e `sessions`; tabelas de negócio, geometrias, permissões e fontes intactas. Conservados 29 fichas, 28 relações e o fingerprint integral das 408 geometrias. Nenhum dossier/pedido sintético criado na homologação. [Recibo da estabilização](map-stabilization-receipt-20261024.json); prova visual em `outputs/map-stabilization-20261024/dondo-installed.png`. O saldo técnico desta reposição está encerrado; a avaliação de navegação e apresentação pela Tempo permanece o próximo trabalho.
Preservados os fechos `d8224ba8`, `55136b44`, `8036b9b0` e `cf2ddf21`/`c1f4a719`, sem repetição das provas aceites. Localidades sem polígono próprio conservam apenas o contexto ancestral explicitamente identificado; não são inventadas coordenadas ou parcelas. Estatísticas e IA permanecem adiadas. Esta estabilização não declara concluída a aplicação transversal nem substitui a avaliação de navegação e apresentação pela Tempo.
## Projectos âncora — entregues na homologação em cf2ddf21, 23/10/2026
Reconciliado o saldo de `6985f82e` com as provas territoriais de 7 de Outubro e os recibos de reposição. A leitura autenticada da homologação, com o papel da aplicação e transacção apenas de leitura, confirmou **29 fichas publicadas, 13 relações de clusters e zero ligações em `project_locations`**. As cinco tabelas comparadas não usam RLS. Os projectos transportados têm identificadores diferentes dos originais e perderam as 28 ligações territoriais e os respectivos metadados de citação; não desapareceram as 29 fichas. A reposição de visibilidade anterior não abrangia estas relações. A comparação delimita a omissão no transporte; não identifica por si só o instante em que ocorreu.
File diff suppressed because one or more lines are too long
@@ -5,6 +5,7 @@ import {
defaultCashflowDraftInputs,
restoreCashflowDraftSnapshot,
serializeCashflowDraftSnapshot,
cashflowCurrency,
type CashflowDraftSnapshot,
} from '@/lib/monthly-cashflow';
import {
@@ -24,6 +25,9 @@ export function SimulationCashflowPlanner({
onSnapshotChange?: (snapshot: string) => void;
}) {
const restored = restoreCashflowDraftSnapshot(snapshot);
const [confirmedCurrency, setConfirmedCurrency] = useState('');
const originCurrency = restored?.version === 2 ? restored.currency : null;
const calculationCurrency = snapshot ? originCurrency ?? '' : currency;
const [taxMode, setTaxMode] = useState<'fixed' | 'schedule'>(
restored?.taxMode ?? 'fixed',
);
@@ -41,7 +45,7 @@ export function SimulationCashflowPlanner({
) {
setInputs(next);
setTaxMode(mode);
onSnapshotChange?.(serializeCashflowDraftSnapshot(mode, next));
onSnapshotChange?.(serializeCashflowDraftSnapshot(mode, next, snapshot ? originCurrency ?? null : currency || null));
}
const set = (
key: Exclude<keyof typeof inputs, 'monthlyTaxSchedule'>,
@@ -63,7 +67,8 @@ export function SimulationCashflowPlanner({
throw new Error(
'Não foi possível recuperar os pressupostos guardados de tesouraria. Reveja os campos antes de voltar a guardar; nenhum resultado foi calculado com dados alternativos.',
);
projection = projectMonthlyCashflow({ ...cashflowInput, currency });
if (restored) cashflowCurrency(restored, currency);
projection = projectMonthlyCashflow({ ...cashflowInput, currency: calculationCurrency });
} catch (cause) {
error =
cause instanceof Error
@@ -76,7 +81,7 @@ export function SimulationCashflowPlanner({
aria-labelledby="cashflow-title"
>
<label className="block max-w-xs text-sm font-medium">
Moeda dos valores (código ISO)
Moeda do projecto (código ISO)
<input
list="cashflow-currency-options"
inputMode="text"
@@ -103,6 +108,16 @@ export function SimulationCashflowPlanner({
<h3 id="cashflow-title" className="text-lg font-semibold">
Projecção de tesouraria — 12 meses
</h3>
{restored && !originCurrency && <div className="space-y-2 rounded border border-amber-600 p-3">
<p>A moeda de origem dos montantes guardados não foi registada. Os valores foram conservados. Confirme apenas essa moeda; não é necessário preencher os montantes novamente.</p>
<label>Moeda de origem da tesouraria guardada<select className="ml-2 rounded border p-2" value={confirmedCurrency} onChange={event => setConfirmedCurrency(event.target.value)}><option value="">Por confirmar</option>{simulationCurrencyOptions.map(code => <option key={code} value={code}>{code}</option>)}</select></label>
<button type="button" className="block underline" disabled={!isSimulationCurrency(confirmedCurrency)} onClick={() => onSnapshotChange?.(serializeCashflowDraftSnapshot(taxMode, inputs, confirmedCurrency))}>Confirmar moeda de origem e conservar montantes</button>
</div>}
{originCurrency && originCurrency !== currency && <div role="alert" className="rounded border border-amber-600 p-3">
<p>{cashflowCurrency(restored!, currency).notice}</p>
<button type="button" className="underline" onClick={() => onCurrencyChange(originCurrency)}>Usar {originCurrency} no projecto e conservar montantes</button>
</div>}
{originCurrency && <p>Moeda de origem dos pressupostos: {originCurrency}.</p>}
<p className="text-sm text-slate-700">
Introduza pressupostos na mesma moeda. O imposto indicado é um
pressuposto do utilizador; esta ferramenta não calcula nem
@@ -257,12 +272,12 @@ export function SimulationCashflowPlanner({
{error}
</p>
)}
{projection && isSimulationCurrency(currency) && (
{projection && isSimulationCurrency(calculationCurrency) && (
<>
<p aria-live="polite" className="font-semibold">
{projection.endingCash === null
? `Caixa conhecida antes de imposto em falta, ao fim de 12 meses (${currency}):`
: `Saldo projectado ao fim de 12 meses (${currency}):`}{' '}
? `Caixa conhecida antes de imposto em falta, ao fim de 12 meses (${calculationCurrency}):`
: `Saldo projectado ao fim de 12 meses (${calculationCurrency}):`}{' '}
{projection.endingCash ??
projection.endingCashBeforeUnassessedTax}
</p>
@@ -274,7 +289,7 @@ export function SimulationCashflowPlanner({
[
exportMonthlyCashflowCsv({
...cashflowInput,
currency,
currency: calculationCurrency,
periods: 12,
}),
],
@@ -1,8 +1,8 @@
import { InvestmentContextAssessment } from './investment-context-assessment';
import { useEffect, useRef, useState } from 'react';
import { monthlyIvaInput, monthlyIvaKey, monthlySalesFromDraft, readMonthlyIvaRecord, readMonthlyIvaSettings, monthlyIvaCashTreatment, type MonthlyIvaInput, type MonthlyIvaSettings } from '@/lib/monthly-iva';
import { monthlyIvaInput, monthlyIvaKey, monthlySalesFromDraft, readMonthlyIvaRecord, readMonthlyIvaSettings, recoverMonthlyIvaRecord, monthlyIvaCashTreatment, type MonthlyIvaInput, type MonthlyIvaSettings } from '@/lib/monthly-iva';
import { fiscalQaRows } from '@/lib/fiscal-qa-preview';
import { isApiSimulationReadResponseV1, type ApiSimulationExecutionV1 } from '@/lib/simulation-contract';
import { type ApiSimulationExecutionV1 } from '@/lib/simulation-contract';
import { SimulationModeloA } from './simulation-modelo-a';
export function SimulationMonthlyIva({draft, busy, onCalculate, onChange}: {
@@ -16,6 +16,9 @@ export function SimulationMonthlyIva({draft, busy, onCalculate, onChange}: {
const [error, setError] = useState('');
const [available, setAvailable] = useState<boolean | null>(null);
const attempt = useRef('');
const mounted = useRef(true);
const latestSnapshot = useRef(draft.monthly_iva_snapshot); latestSnapshot.current = draft.monthly_iva_snapshot;
useEffect(() => { mounted.current = true; return () => { mounted.current = false; }; }, []);
const callbacks = useRef({onCalculate, onChange}); callbacks.current = {onCalculate, onChange};
let input: MonthlyIvaInput | null = null; let inputError = ''; let source = '';
try { source = monthlySalesFromDraft(draft).source; input = monthlyIvaInput(draft, settings); }
@@ -23,6 +26,8 @@ export function SimulationMonthlyIva({draft, busy, onCalculate, onChange}: {
const key = input ? monthlyIvaKey(input) : '';
const current = !!(input && record && monthlyIvaKey(record.input) === key);
const completed = current && record?.execution.status === 'completed';
const pendingRecord = record && ['queued','running'].includes(record.execution.status) ? record : null;
const actionKey = pendingRecord ? `recover:${pendingRecord.execution.id}` : key;
const saveSettings = (patch: Partial<MonthlyIvaSettings>) => onChange('monthly_iva_settings', JSON.stringify({...settings, ...patch}));
useEffect(() => {
let active = true;
@@ -32,27 +37,26 @@ export function SimulationMonthlyIva({draft, busy, onCalculate, onChange}: {
return () => { active = false; };
}, [retry]);
useEffect(() => {
if (!settings.auto || !input || input.currency !== 'MZN' || busy || !available || completed || attempt.current === key) return;
const request = input;
const pending = current && record && ['queued','running'].includes(record.execution.status) ? record.execution.id : null;
if (!settings.auto || busy || attempt.current === actionKey) return;
if (!pendingRecord && (!input || input.currency !== 'MZN' || !available || completed)) return;
const request = pendingRecord?.input ?? input!;
const savedSnapshot = draft.monthly_iva_snapshot;
const timer = window.setTimeout(() => {
attempt.current = key; setError('');
attempt.current = actionKey; setError('');
void (async () => {
try {
let execution;
if (pending) {
const response = await fetch(`/api/v1/simulations/${pending}`, {credentials:'same-origin', headers:{Accept:'application/json'}});
const value: unknown = await response.json();
if (!response.ok || !isApiSimulationReadResponseV1(value)) throw new Error('Não foi possível recuperar a execução mensal guardada. Não foi criada uma execução alternativa.');
execution = value.data;
if (pendingRecord) {
execution = (await recoverMonthlyIvaRecord(pendingRecord)).execution;
} else execution = await callbacks.current.onCalculate(request);
if (!execution) throw new Error('O cálculo mensal não foi concluído. As vendas e a versão anterior foram preservadas; tente novamente.');
if (!mounted.current || (pendingRecord && latestSnapshot.current !== savedSnapshot)) return;
callbacks.current.onChange('monthly_iva_snapshot', JSON.stringify({input:request, execution}));
} catch (cause) {setError(cause instanceof Error ? cause.message : 'Não foi possível calcular o IVA mensal.');}
} catch (cause) {if (mounted.current) setError(cause instanceof Error ? cause.message : 'Não foi possível calcular o IVA mensal.');}
})();
}, 700);
return () => window.clearTimeout(timer);
}, [key, settings.auto, busy, available, completed, retry]);
}, [actionKey, key, settings.auto, busy, available, completed, retry]);
const snapshot = record?.execution.result_snapshot;
const projection = snapshot?.monthly_projection as {rows?: Array<{date:string; sales_declared:string|null; net_sales:string|null; rate:string|null; vat:string|null; gross_sales:string|null; status:string}>} | undefined;
@@ -73,7 +77,8 @@ export function SimulationMonthlyIva({draft, busy, onCalculate, onChange}: {
{settings.auto && !completed && current && <p role="status">Execução mensal guardada: {record?.execution.status === 'queued' ? 'em fila' : record?.execution.status === 'running' ? 'em processamento' : 'não concluída'}. Pode voltar a consultar sem duplicar.</p>}
{busy && <p role="status">A actualizar os resultados dependentes das vendas…</p>}
{error && <p role="alert">{error}</p>}
<button type="button" disabled={busy || !available || !input || input.currency !== 'MZN'} onClick={() => {attempt.current=''; setRetry((value) => value+1); saveSettings({auto:true});}} className="rounded bg-emerald-800 p-2 text-white disabled:opacity-50">Calcular / actualizar IVA mensal e guardar versão</button>
<button type="button" disabled={busy || (!pendingRecord && (!available || !input || input.currency !== 'MZN'))} onClick={() => {attempt.current=''; setRetry((value) => value+1); saveSettings({auto:true});}} className="rounded bg-emerald-800 p-2 text-white disabled:opacity-50">{pendingRecord ? 'Recuperar execução mensal admitida pelo mesmo ID' : 'Calcular / actualizar IVA mensal e guardar versão'}</button>
{pendingRecord && <p>Consulta da execução {pendingRecord.execution.id}. A suspensão de novos cálculos não impede esta recuperação; não é criado outro pedido.</p>}
{settings.auto && <button type="button" className="ml-3 underline" onClick={() => saveSettings({auto:false})}>Pausar actualização automática</button>}
{input && <p className="text-sm">{input.monthly_sales.length} meses; moeda {input.currency || 'por indicar'}. Campo vazio significa informação em falta; zero declarado permanece zero. Depois da primeira execução, alterações das vendas actualizam o cálculo enquanto a actualização automática estiver ligada.</p>}
{projection?.rows && <div className="overflow-x-auto" tabIndex={0}><table className="min-w-full text-sm"><caption>IVA mensal — {current ? 'versão correspondente às entradas' : 'versão histórica'}</caption><thead><tr>{['Mês','Vendas declaradas','Vendas tributáveis sem IVA','Taxa aplicada (%)','IVA calculado','Total com IVA','Estado'].map((label) => <th key={label} className="p-2 text-left">{label}</th>)}</tr></thead><tbody>{projection.rows.map((row) => <tr key={row.date}><th scope="row" className="p-2">{row.date.slice(0,7)}</th><td>{row.sales_declared ?? 'Sem informação'}</td><td>{row.net_sales ?? 'Indeterminado'}</td><td>{row.rate ?? 'Indeterminada'}</td><td>{row.vat ?? 'Indeterminado'}</td><td>{row.gross_sales ?? 'Indeterminado'}</td><td>{{calculated:'Calculado',missing_sales:'Sem informação de vendas',unsupported_basis:'Base líquida não determinada',indeterminate:'Enquadramento ou regra indeterminados'}[row.status] ?? 'Indeterminado'}</td></tr>)}</tbody></table></div>}
+15 -4
View File
@@ -17,7 +17,9 @@ export function defaultCashflowDraftInputs() {
}
export type CashflowDraftSnapshot = {
version: 1;
version: 1 | 2;
/** Missing in legacy v1: never infer it from the current project. */
currency?: string | null;
taxMode: 'fixed' | 'schedule';
inputs: ReturnType<typeof defaultCashflowDraftInputs>;
};
@@ -31,7 +33,8 @@ export function restoreCashflowDraftSnapshot(
const parsed = JSON.parse(snapshot);
if (
!parsed ||
parsed.version !== 1 ||
![1, 2].includes(parsed.version) ||
(parsed.version === 2 && parsed.currency !== null && (typeof parsed.currency !== 'string' || !/^[A-Z]{3}$/.test(parsed.currency))) ||
!['fixed', 'schedule'].includes(parsed.taxMode) ||
!parsed.inputs ||
typeof parsed.inputs !== 'object' ||
@@ -55,7 +58,7 @@ export function restoreCashflowDraftSnapshot(
} else if (typeof value !== 'string' || value.length > 160)
return null;
}
return { version: 1, taxMode: parsed.taxMode, inputs: parsed.inputs };
return { version: parsed.version, ...(parsed.version === 2 ? {currency: parsed.currency} : {}), taxMode: parsed.taxMode, inputs: parsed.inputs };
} catch {
return null;
}
@@ -64,8 +67,16 @@ export function restoreCashflowDraftSnapshot(
export function serializeCashflowDraftSnapshot(
taxMode: CashflowDraftSnapshot['taxMode'],
inputs: CashflowDraftSnapshot['inputs'],
currency?: string | null,
): string {
return JSON.stringify({ version: 1, taxMode, inputs });
return JSON.stringify({ version: currency === undefined ? 1 : 2, ...(currency !== undefined ? {currency} : {}), taxMode, inputs });
}
export function cashflowCurrency(saved: CashflowDraftSnapshot, projectCurrency: string): {currency: string; matchesProject: boolean; notice: string} {
const currency = saved.version === 2 ? saved.currency : null;
if (!currency) throw new Error('A moeda de origem da tesouraria guardada está por confirmar. Os montantes foram preservados; confirme a moeda na ferramenta de tesouraria antes de calcular ou reutilizar as vendas.');
const matchesProject = currency === projectCurrency;
return {currency, matchesProject, notice: matchesProject ? '' : `A tesouraria foi guardada em ${currency}, mas o projecto está em ${projectCurrency || 'moeda por indicar'}. Os montantes continuam em ${currency}; não foram convertidos nem reinterpretados. Para os reutilizar no projecto, restaure explicitamente a moeda de origem. Uma projecção noutra moeda exige pressupostos próprios.`};
}
export type MonthlyCashflowInput = {
+10 -1
View File
@@ -1,5 +1,5 @@
import { monthlyProjectedSales, restoreBusinessPlanSnapshot } from './business-financial-plan.ts';
import { restoreCashflowDraftSnapshot } from './monthly-cashflow.ts';
import { cashflowCurrency, restoreCashflowDraftSnapshot } from './monthly-cashflow.ts';
import { isApiSimulationReadResponseV1, type ApiSimulationExecutionV1 } from './simulation-contract.ts';
export type MonthlyIvaInput = {
@@ -27,6 +27,8 @@ export function monthlySalesFromDraft(draft: Record<string, unknown>): { amounts
if (draft.cashflow_snapshot) {
const saved = restoreCashflowDraftSnapshot(String(draft.cashflow_snapshot));
if (!saved) throw new Error('Não foi possível recuperar a receita projectada.');
const origin = cashflowCurrency(saved, String(draft.currency ?? ''));
if (!origin.matchesProject) throw new Error(origin.notice);
return {amounts: Array.from({length: 12}, () => saved.inputs.monthlyRevenue.trim() === '' ? null : saved.inputs.monthlyRevenue), source: 'Receita mensal constante já declarada na projecção de tesouraria; confirme que corresponde a vendas tributáveis.'};
}
return {amounts: Array.from({length: 12}, () => null), source: 'Sem projecção de vendas guardada. Prepare as vendas no plano empresarial; não é necessário voltar a introduzi-las aqui.'};
@@ -48,4 +50,11 @@ export function readMonthlyIvaRecord(raw?: string): MonthlyIvaRecord | null {
} catch { /* Keep the stored value intact; no fallback fiscal result. */ }
return null;
}
/** Read an admitted execution through the existing ownership-protected endpoint; never create one. */
export async function recoverMonthlyIvaRecord(record: MonthlyIvaRecord, request: typeof fetch = fetch): Promise<MonthlyIvaRecord> {
const response = await request(`/api/v1/simulations/${encodeURIComponent(record.execution.id)}`, {credentials:'same-origin', headers:{Accept:'application/json'}});
const value: unknown = await response.json();
if (!response.ok || !isApiSimulationReadResponseV1(value) || value.data.id !== record.execution.id) throw new Error('Não foi possível recuperar a execução mensal guardada. Os dados foram preservados e não foi criada uma execução alternativa.');
return {input: record.input, execution: value.data};
}
export const monthlyIvaCashTreatment = 'IVA apresentado separadamente: não é somado novamente aos recebimentos declarados nem deduzido como custo ou imposto final a pagar. A tesouraria conserva os recebimentos e pagamentos introduzidos, antes de uma liquidação fiscal não apurada; não se presume cobrança integral das vendas no mesmo mês.';
+4 -1
View File
@@ -7,6 +7,7 @@ import { fiscalQaRows } from './fiscal-qa-preview.ts';
import { csvCellWithDelimiter } from './csv.js';
import {
restoreCashflowDraftSnapshot,
cashflowCurrency,
projectMonthlyCashflow,
} from './monthly-cashflow.ts';
import {
@@ -637,7 +638,9 @@ export function buildProjectDossierRows(
throw new Error(
'Pressupostos de tesouraria não recuperáveis; não foram substituídos por valores alternativos.',
);
const currency = String(draft.currency ?? '');
const originCurrency = cashflowCurrency(saved, String(draft.currency ?? ''));
const currency = originCurrency.currency;
if (originCurrency.notice) rows.push(['Tesouraria', 'Divergência de moeda', originCurrency.notice, 'Moeda original preservada; sem conversão']);
if (!currency)
throw new Error(
'Indique uma moeda para interpretar os montantes de tesouraria.',
@@ -0,0 +1,67 @@
<?php
namespace Tests\Feature;
use App\Jobs\RunSimulation;
use App\Models\Simulation;
use App\Models\SimulationTaxRuleVersion;
use App\Models\User;
use App\Services\SimulationExecution;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Queue;
use Illuminate\Support\Str;
use Tests\TestCase;
final class SimulatorClosureRecoveryTest extends TestCase
{
use DatabaseTransactions;
public function test_existing_monthly_admission_can_be_read_and_finished_when_new_admissions_are_off(): void
{
$identity = DB::selectOne('select current_database() as name,inet_server_port() as port');
$this->assertSame('simulator_fiscal_delta_20261021_test', $identity->name);
$this->assertSame(55441, (int) $identity->port);
Queue::fake();
// Reuse the existing isolated synthetic rule. No create/publish/suspend of any rule.
$rule = SimulationTaxRuleVersion::findOrFail('01a15e50-7972-72e0-8991-82ca28df9b49');
$before = $this->ruleFingerprint();
config(['simulation-fiscal-consumer.enabled' => true, 'simulation-fiscal-consumer.rule_id' => $rule->id, 'simulation-fiscal-consumer.rule_version' => $rule->rule_version]);
$actor = User::factory()->create(['email_verified_at' => now()]);
$input = ['scenario_kind' => 'fiscal_rule_published', 'currency' => 'MZN', 'scope_declared' => true, 'sales_vat_basis' => 'net', 'monthly_sales' => [['date' => '2026-01-01', 'amount' => '1000.03']]];
$id = $this->actingAs($actor)->postJson('/api/v1/simulations', $input, ['Idempotency-Key' => (string) Str::uuid()])->assertAccepted()->json('data.id');
$count = Simulation::count();
config(['simulation-fiscal-consumer.enabled' => false]);
$this->getJson('/api/v1/fiscal-consumer')->assertOk()->assertJsonPath('data.enabled', false);
$this->getJson('/api/v1/simulations/'.$id)->assertOk()->assertJsonPath('data.id', $id)->assertJsonPath('data.status', 'queued');
$this->postJson('/api/v1/simulations', $input, ['Idempotency-Key' => (string) Str::uuid()])->assertStatus(503);
(new RunSimulation($id, $actor->id, null))->handle(app(SimulationExecution::class));
$this->getJson('/api/v1/simulations/'.$id)->assertOk()->assertJsonPath('data.id', $id)->assertJsonPath('data.status', 'completed');
$this->assertSame($count, Simulation::count());
$this->actingAs(User::factory()->create(['email_verified_at' => now()]))->getJson('/api/v1/simulations/'.$id)->assertNotFound();
$this->assertSame($before, $this->ruleFingerprint());
}
private function ruleFingerprint(): string
{
return DB::selectOne("select md5(string_agg(row_to_json(r)::text,E'\\n' order by id)) as hash from simulation_tax_rule_versions r")->hash;
}
public function test_cashflow_original_currency_and_amounts_round_trip_in_the_existing_private_draft(): void
{
$identity = DB::selectOne('select current_database() as name,inet_server_port() as port');
$this->assertSame('simulator_fiscal_delta_20261021_test', $identity->name);
$this->assertSame(55441, (int) $identity->port);
config(['investor-path.enabled' => true, 'suitecrm.enabled' => false]);
$actor = User::factory()->create(['email_verified_at' => now()]);
$this->actingAs($actor);
$id = $this->postJson('/investimentos', ['title' => 'SYNTHETIC QA — moeda de tesouraria', 'client_key' => (string) Str::uuid(), 'reuse_consent' => true])->assertCreated()->json('id');
$snapshot = json_encode(['version' => 2, 'currency' => 'USD', 'taxMode' => 'fixed', 'inputs' => ['monthlyRevenue' => '123', 'monthlyTaxSchedule' => ['0', '', '0']]], JSON_THROW_ON_ERROR);
$this->postJson('/api/v1/simulation-drafts', ['context' => $id, 'personal_snapshot' => [], 'economic_snapshot' => ['purpose' => 'simulate_only', 'project_title' => 'SYNTHETIC QA — moeda', 'currency' => 'MZN', 'cashflow_snapshot' => $snapshot]], ['Idempotency-Key' => (string) Str::uuid()])->assertCreated();
$this->actingAs($actor->fresh());
$saved = $this->getJson('/api/v1/simulation-drafts/current?context='.$id)->assertOk()->json('data.economic_snapshot');
$this->assertSame('MZN', $saved['currency']);
$this->assertSame($snapshot, $saved['cashflow_snapshot']);
$this->assertSame('USD', json_decode($saved['cashflow_snapshot'], true)['currency']);
}
}
@@ -0,0 +1,118 @@
<?php
namespace Tests\Feature;
use App\Models\User;
use App\Services\ApprovedAdministrativeBoundaries;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Illuminate\Routing\Middleware\ThrottleRequests;
use Illuminate\Support\Facades\Crypt;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;
use Tests\TestCase;
final class TerritoryGeometryConsumerAccessTest extends TestCase
{
use DatabaseTransactions;
private User $actor;
private string $territory;
protected function setUp(): void
{
parent::setUp();
$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->withoutVite();
$this->withoutMiddleware(ThrottleRequests::class);
config(['portal.profile_access_enabled' => true, 'portal.managers' => [], 'investor-path.enabled' => true, 'suitecrm.enabled' => false, 'investmoz_ai.enabled' => false]);
$this->actor = User::factory()->create(['two_factor_secret' => Crypt::encryptString('JBSWY3DPEHPK3PXP'), 'two_factor_confirmed_at' => now()]);
$other = User::factory()->create();
DB::table('portal_superuser_pair')->updateOrInsert(['id' => 1], ['first_user_id' => $this->actor->id, 'second_user_id' => $other->id, 'approved_by' => $other->id]);
$this->territory = DB::table('geo_geometries as g')->join('geo_locations as l', 'l.id', '=', 'g.geo_location_id')
->where('g.source_system', 'ine-cod-ab')->where('g.data_classification', 'internal')->where('l.location_type', 'province')->value('l.id');
$this->assertNotEmpty($this->territory);
}
public function test_general_consumer_uses_existing_authenticated_read_access_without_the_legacy_gate(): void
{
$this->assertFalse($this->actor->can('manage-portal'));
$before = $this->fingerprint();
$this->actingAs($this->actor)->getJson('/api/v1/investment/territory-geometries?territory_id='.$this->territory)
->assertOk()->assertJsonPath('selected_geometry_available', true)->assertJsonPath('geometry_scope', 'authorized_internal')
->assertJsonPath('areas.0.territory_id', $this->territory)->assertHeader('Cache-Control', 'no-store, private');
$this->assertSame($before, $this->fingerprint());
}
public function test_existing_administration_reaches_the_same_geometry_with_its_completed_mfa_session(): void
{
$this->actingAs($this->actor)->withSession(['portal.mfa_actor' => $this->actor->id, 'portal.mfa_at' => time(), 'portal.access_revision' => (int) $this->actor->portal_access_revision]);
$this->getJson('/management/territory-geometries?territory_id='.$this->territory)->assertOk()->assertJsonPath('selected_geometry_available', true);
session()->forget(['portal.mfa_actor', 'portal.mfa_at']);
$this->getJson('/management/territory-geometries?territory_id='.$this->territory)->assertForbidden();
}
public function test_anonymous_and_ordinary_accounts_do_not_receive_or_publish_internal_layers(): void
{
$before = $this->fingerprint();
$url = '/api/v1/investment/territory-geometries?territory_id='.$this->territory;
$this->getJson($url)->assertOk()->assertJsonPath('geometry_scope', 'public')->assertJsonPath('selected_geometry_available', false)->assertJsonCount(0, 'areas');
$this->actingAs(User::factory()->create())->getJson($url)->assertOk()->assertJsonPath('geometry_scope', 'public')->assertJsonCount(0, 'areas');
$this->getJson('/management/territory-geometries?territory_id='.$this->territory)->assertForbidden();
$this->assertSame($before, $this->fingerprint());
}
public function test_saved_territory_is_recovered_in_dossier_and_simulator_and_the_map_uses_that_identity(): void
{
$this->actingAs($this->actor);
$id = $this->postJson('/investimentos', ['title' => 'SYNTHETIC QA - mapa recuperado', 'client_key' => (string) Str::uuid(), 'reuse_consent' => true])->assertCreated()->json('id');
$body = ['version' => $this->getJson('/investimentos/'.$id.'/contexto')->json('case.version'), 'selection' => ['territory' => 'territory:'.$this->territory]];
$preview = $this->postJson('/investimentos/'.$id.'/contexto/previa', $body)->assertOk()->json();
$this->postJson('/investimentos/'.$id.'/contexto/confirmar', [...$body, 'preview_hash' => $preview['preview_hash'], 'operation_key' => (string) Str::uuid(), 'confirm' => true])->assertOk();
$view = $this->getJson('/investimentos/'.$id.'/contexto')->assertOk()->json();
$this->assertSame($this->territory, $view['context']['selection']['territory']['id']);
$this->assertSame($this->territory, $view['prefill']['simulator']['province_id']);
$this->getJson('/api/v1/investment/territory-geometries?territory_id='.$view['prefill']['simulator']['province_id'])
->assertOk()->assertJsonPath('selected_geometry_available', true)->assertJsonPath('areas.0.selected_territory', true);
$this->assertSame($this->territory, $this->getJson('/investimentos/'.$id.'/contexto')->json('context.selection.territory.id'));
}
public function test_only_the_approved_unchanged_snapshot_is_public_without_reclassifying_the_original(): void
{
$before = $this->fingerprint();
$row = DB::table('geo_geometries as g')->where('geo_location_id', $this->territory)->first([
'g.id', 'g.geo_location_id', DB::raw("encode(sha256(ST_AsEWKB(g.geom)),'hex') as geometry_sha256"),
DB::raw("encode(sha256(convert_to(g.properties::text,'UTF8')),'hex') as properties_sha256"),
]);
// The QA import has its own identities; never copy or replace production rows.
$approved = $this->partialMock(ApprovedAdministrativeBoundaries::class);
$approved->shouldReceive('snapshots')->andReturn([$row->id => [
'territory_id' => $row->geo_location_id, 'geometry_sha256' => $row->geometry_sha256,
'properties_sha256' => $row->properties_sha256,
]]);
$url = '/api/v1/investment/territory-geometries?territory_id='.$this->territory;
$this->getJson($url)->assertOk()->assertJsonPath('selected_geometry_available', true)
->assertJsonPath('areas.0.data_classification', 'public')->assertJsonPath('investment_site', false);
$this->actingAs(User::factory()->create())->getJson($url)->assertOk()->assertJsonPath('selected_geometry_available', true);
$this->assertSame($before, $this->fingerprint());
$this->assertSame('internal', DB::table('geo_geometries')->where('id', $row->id)->value('data_classification'));
DB::table('geo_geometries')->where('id', $row->id)->update(['properties' => DB::raw("properties || '{\"qa_changed\":true}'::jsonb")]);
$this->getJson($url)->assertOk()->assertJsonPath('selected_geometry_available', false)->assertJsonCount(0, 'areas');
$this->actingAs($this->actor)->getJson($url)->assertOk()->assertJsonPath('selected_geometry_available', true);
}
public function test_public_manifest_has_the_exact_approved_scope_and_immutable_hash(): void
{
$items = app(ApprovedAdministrativeBoundaries::class)->snapshots();
$this->assertCount(408, $items);
$this->assertCount(408, array_unique(array_column($items, 'territory_id')));
$this->assertSame(['2019-06-07'], array_values(array_unique(array_column($items, 'source_version'))));
}
private function fingerprint(): string
{
return DB::selectOne("select md5(string_agg(row_to_json(g)::text,E'\\n' order by id)) as hash from geo_geometries g")->hash;
}
}
@@ -0,0 +1,30 @@
<?php
namespace Tests\Unit;
use App\Services\ApprovedAdministrativeBoundaries;
use Tests\TestCase;
/** Runs in the normal delivery unit suite, without database or external services. */
final class ApprovedAdministrativeBoundariesTest extends TestCase
{
public function test_public_scope_keeps_the_accepted_identity_and_refuses_changed_or_new_internal_shapes(): void
{
$approved = new ApprovedAdministrativeBoundaries;
$items = $approved->snapshots();
$this->assertCount(408, $items);
$item = reset($items);
$this->assertTrue($approved->allows($item['id'], $item['territory_id'], $item['geometry_sha256'], $item['properties_sha256']));
$this->assertFalse($approved->allows('00000000-0000-0000-0000-000000000000', $item['territory_id'], $item['geometry_sha256'], $item['properties_sha256']));
$this->assertFalse($approved->allows($item['id'], '00000000-0000-0000-0000-000000000000', $item['geometry_sha256'], $item['properties_sha256']));
$this->assertFalse($approved->allows($item['id'], $item['territory_id'], str_repeat('0', 64), $item['properties_sha256']));
$this->assertFalse($approved->allows($item['id'], $item['territory_id'], $item['geometry_sha256'], str_repeat('0', 64)));
}
public function test_the_public_scope_keeps_the_accepted_edition_and_unique_territories(): void
{
$items = (new ApprovedAdministrativeBoundaries)->snapshots();
$this->assertCount(408, array_unique(array_column($items, 'territory_id')));
$this->assertSame(['2019-06-07'], array_values(array_unique(array_column($items, 'source_version'))));
}
}
@@ -0,0 +1,13 @@
import {serializeBusinessPlanSnapshot} from '../../../resources/js/lib/business-financial-plan.ts';
export function closureFixture(status='completed') {
const fields=['openingCash','monthlyCollections','monthlyVariableCostPayments','monthlyInventoryPurchases','monthlyOperatingExpenses','monthlyOperatingPayments','monthlyPayrollExpense','monthlyPayrollPayments','assetInvestment','inventory','receivables','payables','equity','shareholderLoans','bankLoanProceeds','grants','assetDepreciation','monthlyInterestExpense','monthlyInterestPayment','monthlyPrincipalRepayment'];
const plan={...Object.fromEntries(fields.map(k=>[k,'0'])),currency:'MZN',periods:4,depreciationLifeMonths:60,products:[{label:'SYNTHETIC QA',monthlyUnits:'1',unitPrice:'1000.03',unitVariableCost:'0',monthlyUnitsByPeriod:['1','2','0','']}]};
const input={scenario_kind:'fiscal_rule_published',monthly_sales:[{date:'2026-01-01',amount:'1000.03'},{date:'2026-02-01',amount:'2000.06'},{date:'2026-03-01',amount:'0.00'},{date:'2026-04-01',amount:null}],sales_vat_basis:'net',currency:'MZN',scope_declared:true};
// Frozen numbers already accepted in the monthly QA receipt; no tax calculation here.
const rows=[{date:'2026-01-01',sales_declared:'1000.03',net_sales:'1000.03',rate:'16',vat:'160.0048',gross_sales:'1160.0348',status:'calculated'},{date:'2026-02-01',sales_declared:'2000.06',net_sales:'2000.06',rate:'16',vat:'320.0096',gross_sales:'2320.0696',status:'calculated'},{date:'2026-03-01',sales_declared:'0.00',net_sales:'0.00',rate:'16',vat:'0',gross_sales:'0',status:'calculated'},{date:'2026-04-01',sales_declared:null,net_sales:null,rate:null,vat:null,gross_sales:null,status:'missing_sales'}];
const snapshot={contract_version:1,scenario_kind:'fiscal_rule_published',base:null,currency:'MZN',indicative_total:null,legal_interpretation:false,formula_version:'investmoz.tax.v1',context:{reference_date:'2026-01-01',currency:'MZN',currency_fraction_digits:2,scenario_id:'61000100-0000-4000-8000-000000000099',scenario_version:1,engine_version:'fiscal-published-v1',ruleset_version:'IVA-GERAL-BASE-LIQUIDA:4'},result:{status:'indeterminate',value:null,unit:'MZN',explanation:'IVA das vendas projectadas; sem apuramento final.',missing_inputs:['monthly_sales'],source_ids:['synthetic-qa-source']},presentation:{value:null,rounding_applied:false},fiscal_provenance:{rule:{rule_code:'IVA-GERAL-BASE-LIQUIDA',rule_version:4,valid_from:'2023-01-01',valid_until:null,source_sha256:'synthetic-qa-source-hash',source:{diploma:'Fonte sintética QA — regra já ensaiada',article:'Cópia do snapshot aceite'}},scope_status:'user_declared'},monthly_projection:{input,rows,cash_treatment:'IVA separado da tesouraria; não é imposto final a pagar.'}};
const execution={id:'06566910-9672-4176-804d-aad8350417cb',status,version_no:2,result_snapshot:status==='completed'?snapshot:null};
const context={version:1,context_revision:13,consumer_opportunity:'opportunity:309',opportunity_choices:[{opportunity:{key:'opportunity:309',title:'SYNTHETIC QA — segunda oportunidade Manica'}}],path:{steps:[]}};
return {input,snapshot,execution,draft:{project_title:'SYNTHETIC QA — fecho do simulador S1 S2 S3',currency:'MZN',context_opportunity:'opportunity:309',context_applicability_snapshot:JSON.stringify(context),business_plan_snapshot:serializeBusinessPlanSnapshot(plan),monthly_iva_settings:JSON.stringify({start:'2026-01',basis:'net',declared:true,auto:false}),monthly_iva_snapshot:JSON.stringify({input,execution})}};
}
@@ -0,0 +1,55 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import {mkdir,writeFile} from 'node:fs/promises';
import JSZip from 'jszip';
import {defaultCashflowDraftInputs,serializeCashflowDraftSnapshot,restoreCashflowDraftSnapshot,cashflowCurrency} from '../../resources/js/lib/monthly-cashflow.ts';
import {monthlyIvaInput,recoverMonthlyIvaRecord} from '../../resources/js/lib/monthly-iva.ts';
import {exportProjectDossierCsv,buildProjectDossierRows} from '../../resources/js/lib/project-dossier.ts';
import {createProjectDossierReport,generateProjectDossierDocx,createProjectDossierPrintHtml} from '../../resources/js/lib/project-dossier-report.ts';
import {closureFixture} from './fixtures/simulator-closure-fixture.mjs';
test('S1 saved USD stays USD in report and cannot be reused as MZN for IVA',()=>{
const inputs={...defaultCashflowDraftInputs(),monthlyRevenue:'123',monthlyTaxAssessed:'0'};
const snapshot=serializeCashflowDraftSnapshot('fixed',inputs,'USD');
const draft={currency:'MZN',cashflow_snapshot:snapshot};
const saved=restoreCashflowDraftSnapshot(snapshot);
assert.equal(saved.currency,'USD');assert.deepEqual(saved.inputs,inputs);
assert.equal(cashflowCurrency(saved,'MZN').matchesProject,false);
assert.throws(()=>monthlyIvaInput(draft,{start:'2026-01',basis:'net',declared:true,auto:false}),/continuam em USD/);
const rows=buildProjectDossierRows(draft).filter(r=>r[0]==='Tesouraria');
assert.match(rows.find(r=>r[1]==='Pressupostos e período')[2],/moeda USD/);
assert.match(rows.find(r=>r[1]==='Divergência de moeda')[2],/não foram convertidos/);
const restoredProject={...draft,currency:'USD'};
assert.equal(cashflowCurrency(restoreCashflowDraftSnapshot(restoredProject.cashflow_snapshot),restoredProject.currency).matchesProject,true);
assert.equal(restoredProject.cashflow_snapshot,snapshot);
});
test('S1 legacy amounts remain readable but require explicit origin currency before reuse',()=>{
const inputs={...defaultCashflowDraftInputs(),monthlyRevenue:'123'};
const legacy=serializeCashflowDraftSnapshot('fixed',inputs);const saved=restoreCashflowDraftSnapshot(legacy);
assert.equal(saved.version,1);assert.throws(()=>cashflowCurrency(saved,'MZN'),/por confirmar/);
const confirmed=restoreCashflowDraftSnapshot(serializeCashflowDraftSnapshot(saved.taxMode,saved.inputs,'USD'));
assert.deepEqual(confirmed.inputs,saved.inputs);assert.equal(confirmed.currency,'USD');
assert.equal(restoreCashflowDraftSnapshot(legacy).version,1);
});
test('S2 recovery is a same-ID authenticated GET without new admission and preserves denied records',async()=>{
const pending=closureFixture('queued');const completed=closureFixture();const calls=[];
const record={input:pending.input,execution:pending.execution};const before=JSON.stringify(record);
const request=async(url,options)=>{calls.push({url,options});return new Response(JSON.stringify({data:completed.execution}),{status:200});};
const recovered=await recoverMonthlyIvaRecord(record,request);
assert.equal(recovered.execution.id,pending.execution.id);assert.equal(recovered.execution.status,'completed');assert.deepEqual(recovered.input,pending.input);
assert.equal(calls.length,1);assert.equal(calls[0].options.credentials,'same-origin');assert.equal(calls[0].options.method,undefined);assert.match(calls[0].url,/simulations\/06566910/);
await assert.rejects(()=>recoverMonthlyIvaRecord(record,async()=>new Response('{}',{status:403})),/preservados/);
await assert.rejects(()=>recoverMonthlyIvaRecord(record,async()=>new Response(JSON.stringify({data:{...completed.execution,id:'00000000-0000-4000-8000-000000000000'}}),{status:200})),/preservados/);
assert.equal(JSON.stringify(record),before);
});
test('S3 final CSV Word and print source retain frozen monthly values context source and version',async()=>{
const {draft}=closureFixture();
const report=createProjectDossierReport(draft,{selectedSections:new Set(['sources','finance']),version:'SYNTHETIC QA S3'});
const csv=exportProjectDossierCsv(draft);const html=await createProjectDossierPrintHtml(report,[],false);
const docx=await generateProjectDossierDocx(report,[],true);const bytes=new Uint8Array(await docx.arrayBuffer());const word=await JSZip.loadAsync(bytes);const xml=await word.file('word/document.xml').async('string');
for(const text of [csv,html,xml]) for(const expected of ['160.0048','320.0096','1160.0348','MZN','2026-01','2026-04','Fonte sintética QA','IVA-GERAL-BASE-LIQUIDA','segunda oportunidade Manica','opportunity:309']) assert.ok(text.includes(expected),expected);
for(const text of [csv,html,xml]) assert.match(text,/IVA-GERAL-BASE-LIQUIDA[^\n<]{0,30}4/);
assert.match(html,/segunda oportunidade Manica/);assert.match(xml,/segunda oportunidade Manica/);
const out='outputs/simulator-closure-qa-20261024';await mkdir(out,{recursive:true});
await Promise.all([writeFile(`${out}/monthly-final.csv`,csv),writeFile(`${out}/monthly-final.docx`,bytes),writeFile(`${out}/monthly-final.html`,html),writeFile(`${out}/frozen-fixture.json`,JSON.stringify(draft,null,2))]);
});
@@ -17,7 +17,7 @@ test('cashflow draft recovery preserves unanswered months and invalid editable v
openingCash: '77.01',
monthlyTaxSchedule: ['10', '', '20', ...Array(9).fill('0')],
};
const stored = serializeCashflowDraftSnapshot('schedule', inputs);
const stored = serializeCashflowDraftSnapshot('schedule', inputs, 'MZN');
const restored = restoreCashflowDraftSnapshot(stored);
assert.equal(restored.taxMode, 'schedule');
assert.deepEqual(restored.inputs, inputs);
@@ -65,7 +65,7 @@ test('cashflow draft recovery preserves unanswered months and invalid editable v
assert.equal(restoreCashflowDraftSnapshot('{"version":2}'), null);
assert.equal(
restoreCashflowDraftSnapshot(
stored.replace('"version":1', '"version":2'),
stored.replace('"version":2', '"version":3'),
),
null,
);