feat: display precise public labour and economic observations in map context
tests / ci (push) Canceled after 0s
tests / ci (push) Canceled after 0s
This commit is contained in:
@@ -9,6 +9,8 @@ use App\Services\CorridorAgribusinessOpportunities;
|
||||
use App\Services\InvestmentCatalogue;
|
||||
use App\Services\InvestmentComplements;
|
||||
use App\Services\InvestmentGeography;
|
||||
use App\Services\InvestmentMapIndicators;
|
||||
use App\Services\InvestmentZoneOperationalDetails;
|
||||
use App\Services\InvestmentZones;
|
||||
use App\Services\OpportunityCorridorExplorer;
|
||||
use App\Services\OpportunityTextSearch;
|
||||
@@ -155,6 +157,7 @@ class InvestmentDiscoveryController extends Controller
|
||||
$filters = $catalogue->filters($request);
|
||||
$context = $this->territorial($filters)->map(fn ($d) => ['id' => 'dossier:'.$d->id, 'title' => $d->title, 'kind' => $d->cluster_id ? 'Cluster' : ($d->corridor_id ? 'Corredor' : 'Projeto âncora'), 'url' => '/carteira/'.$d->id]);
|
||||
$opportunityOnlyFilters = array_filter(Arr::only($filters, ['association_type', 'association_target', 'association_state', 'association_purpose', 'authority_id', 'chain_id', 'subsector_id', 'category', 'publication_state', 'education', 'sector', 'facets', 'project_id', 'cluster_id', 'corridor_id', 'min_amount', 'max_amount', 'currency', 'before', 'maturity_level', 'preparation']), fn ($value) => $value !== null && $value !== '' && $value !== false && $value !== []);
|
||||
$quantitative = ['state' => 'not_included', 'records' => [], 'truncated' => false];
|
||||
if ($opportunityOnlyFilters === [] && ! in_array($filters['dimension'] ?? null, ['cluster', 'corridor'], true)) {
|
||||
$zoneFilters = $filters;
|
||||
if (in_array($filters['dimension'] ?? null, ['zee', 'zfi'], true)) {
|
||||
@@ -166,10 +169,12 @@ class InvestmentDiscoveryController extends Controller
|
||||
$zones->where('id', $filters[$key]);
|
||||
}
|
||||
}
|
||||
$context = $context->concat($zones->orderBy('name')->limit(30)->get(['id', 'name', 'category'])->map(fn ($z) => ['id' => 'zone:'.$z->id, 'title' => $z->name, 'kind' => ['zee' => 'ZEE', 'zfi' => 'ZFI', 'industrial_park' => 'Parque industrial'][$z->category] ?? $z->category, 'url' => '/zonas/'.$z->id]));
|
||||
$zoneRows = $zones->orderBy('name')->orderBy('id')->limit(31)->get(['id', 'name', 'category', 'version_no', ...(app(InvestmentZoneOperationalDetails::class)->installed() ? ['operational_details'] : [])]);
|
||||
$quantitative = app(InvestmentMapIndicators::class)->forZones($zoneRows->take(30)->values(), $zoneRows->count() > 30);
|
||||
$context = $context->concat($zoneRows->take(30)->map(fn ($z) => ['id' => 'zone:'.$z->id, 'title' => $z->name, 'kind' => ['zee' => 'ZEE', 'zfi' => 'ZFI', 'industrial_park' => 'Parque industrial'][$z->category] ?? $z->category, 'url' => '/zonas/'.$z->id]));
|
||||
}
|
||||
|
||||
return response()->json([...$catalogue->map($filters), 'territorial_context' => $context->values(), 'context_limit_per_collection' => 30]);
|
||||
return response()->json([...$catalogue->map($filters), 'territorial_context' => $context->values(), 'quantitative_context' => $quantitative, 'context_limit_per_collection' => 30]);
|
||||
}
|
||||
|
||||
public function compare(Request $request, InvestmentCatalogue $catalogue): JsonResponse
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Models\InvestmentZone;
|
||||
use App\Support\StructuredData;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
/** Public park observations remain individual source records, never regional estimates. */
|
||||
final class InvestmentMapIndicators
|
||||
{
|
||||
public const INDICATORS = ['employment', 'investment', 'production', 'exports', 'local_procurement'];
|
||||
|
||||
/** @param Collection<int, InvestmentZone> $zones
|
||||
* @return array<string,mixed> */
|
||||
public function forZones(Collection $zones, bool $zoneLimitReached = false): array
|
||||
{
|
||||
$zoneLimitReached = $zoneLimitReached || $zones->count() > 30;
|
||||
$zones = $zones->take(30)->values();
|
||||
$details = app(InvestmentZoneOperationalDetails::class);
|
||||
$result = ['state' => $details->installed() ? 'available' : 'unavailable', 'records' => [],
|
||||
'zones_consulted' => $zones->count(), 'zone_limit' => 30, 'record_limit' => 200, 'truncated' => $zoneLimitReached];
|
||||
if ($result['state'] === 'unavailable') {
|
||||
return $result;
|
||||
}
|
||||
foreach ($zones as $zone) {
|
||||
// This is the already selected public envelope; never decrypt the private preparation.
|
||||
$public = $details->publicData($zone);
|
||||
$observations = StructuredData::collectionItems($public['integral']['observations'] ?? []);
|
||||
$values = [];
|
||||
if ($observations !== []) {
|
||||
// Extract decimal text from this exact loaded snapshot, not a newer database version or a PHP float.
|
||||
foreach (DB::select("SELECT item->>'id' AS id, item->>'value' AS value FROM jsonb_array_elements((?::jsonb)->'public'->'integral'->'observations') AS item", [$zone->getRawOriginal('operational_details')]) as $number) {
|
||||
$values[$number->id] = $number->value;
|
||||
}
|
||||
}
|
||||
foreach ($observations as $row) {
|
||||
if (! in_array($row['indicator'] ?? null, self::INDICATORS, true)) {
|
||||
continue;
|
||||
}
|
||||
if (count($result['records']) === 200) {
|
||||
$result['truncated'] = true;
|
||||
|
||||
return $result;
|
||||
}
|
||||
$knowledge = $row['knowledge'] ?? 'unknown';
|
||||
$rawValue = $values[$row['id'] ?? ''] ?? null;
|
||||
$value = $knowledge !== 'unknown' && is_numeric($rawValue) ? $rawValue : null;
|
||||
$missing = [];
|
||||
foreach (['unit', 'period_start', 'period_end', 'scope', 'population', 'methodology', 'source_reference', 'source_date'] as $field) {
|
||||
if (trim((string) ($row[$field] ?? '')) === '') {
|
||||
$missing[] = $field;
|
||||
}
|
||||
}
|
||||
$result['records'][] = [
|
||||
...array_intersect_key($row, array_flip(['id', 'label', 'description', 'indicator', 'unit', 'period_start', 'period_end', 'scope', 'population', 'methodology', 'source_reference', 'source_date', 'phase_id', 'plot_id'])),
|
||||
'zone_id' => $zone->id, 'zone_name' => $zone->name, 'zone_version' => (int) $zone->version_no, 'url' => '/zonas/'.$zone->id,
|
||||
'theme' => $row['indicator'] === 'employment' ? 'workforce' : 'economic_potential',
|
||||
'value' => $value, 'knowledge' => $knowledge, 'missing_metadata' => $missing,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
@@ -2258,3 +2258,20 @@ Delta aprovado: sete testes de BD/77 asserções (cinco do mapa, incluindo o nov
|
||||
Conferência visual no Edge exclusivamente na QA `127.0.0.2:18644`/`main_visual_20261104_test`: contexto económico reutiliza a evidência de actividade já publicada da ficha 383, sintética e explicitamente identificada. Mão-de-obra usa uma única fixture nova, também sintética; escolha e fonte recuperadas após recarregar. Nenhuma coordenada/geometria criada; hash de todas as oportunidades anteriores idêntico antes/depois. Conferidos 36 artefactos dos três recibos anteriores, todos com SHA256 inalterado. Recibo: `docs/investmoz/unification/workforce-economic-map-local-20261104.json`; inventário, DOM, capturas e hashes em `outputs/workforce-economic-map-20261104/`.
|
||||
|
||||
**ACT-02 permanece parcial.** Cobertura factual laboral/económica, séries, unidades e metodologia quantitativa comparável, georreferenciação própria documentada e cobertura cartográfica continuam pendentes. A demonstração sintética não mede oferta laboral nem potencial regional real. Mantêm-se conferência remota e aceitação da equipa; V2-07/V2-10 não encerrados. Sem push, migração, publicação automática, escrita activa, alteração de permissões ou reinício de serviços.
|
||||
|
||||
|
||||
### 04/11/2026 — ACT-20261029-02: observações quantitativas públicas de emprego e contexto económico
|
||||
|
||||
Incremento na main local sobre `a9241ff2`. Inventário prévio: fichas de parques já conservam observações com valor, unidade, período, âmbito, população/base, metodologia, conhecimento e divulgação seleccionada. Reutilizada apenas a versão pública de `operational_details.public.integral.observations`, sem desencriptar preparação privada. Na cópia QA havia oito fichas públicas e zero observações quantitativas divulgadas; não foi declarada perda nem ausência no remoto. As 51 provas anteriores foram conferidas por SHA256 antes da ronda e novamente no fim, todas inalteradas; bateria geral aceite preservada sem repetição integral.
|
||||
|
||||
A API de mapa acrescenta `quantitative_context` a partir do mesmo conjunto de zonas já devolvido no contexto territorial: emprego para mão-de-obra; investimento, produção, exportações e compras locais para contexto económico. Reutiliza os filtros/autorização existentes, sem alargar a pesquisa de oportunidades nem transformar indicadores do parque em indicadores das oportunidades. Filtros específicos que já excluem esse conjunto devolvem âmbito não incluído, em vez de uma falsa medição vazia. Até 30 fichas e 200 observações; truncagem explícita. Geometrias, divulgação, fontes e versões anteriores conservadas.
|
||||
|
||||
O painel do mapa mostra cada valor com a sua unidade, período, âmbito, população/base, metodologia, fonte/data, versão pública e ligação à ficha. Zero documentado e valor desconhecido são distintos; precisão decimal conserva-se como texto. Divergências e metadados em falta permanecem visíveis. Emprego não é oferta de trabalhadores disponíveis; natureza prevista/efectiva depende da descrição da fonte. Não se somam populações ou períodos, não se convertem unidades/moedas e não se criam médias, índices ou previsões de retorno. Contagens de oportunidades e de observações territoriais têm âmbitos distintos. Erro, carregamento, cadastro indisponível, âmbito não consultado e ausência de observações são estados diferentes.
|
||||
|
||||
Verificação dirigida aprovada: 11 testes BD/99 asserções (seis novos e cinco do mapa temático existente), 18 testes frontend (três novos e 15 existentes), PHPStan sem erros, tipos, Pint, lint e compilação QA aprovados. Inclui zero/decimais, desconhecido/divergência, metadados incompletos, exclusão de preparação privada/ficha não publicada, preservação do envelope, província/distrito, limites de observações/fichas e âmbito excluído. Primeira tentativa teve uma expectativa inadequada sobre a serialização JSON antes da normalização jsonb; comparação corrigida para duas leituras efectivas do servidor. Fixture frontend com rótulo repetido corrigida; não se relaxaram regras do produto. Logs BD iniciais mantidos.
|
||||
|
||||
Conferência visual exclusivamente em `127.0.0.2:18644`/`main_visual_20261104_test`: uma ficha sintética nova com cinco observações. Camada laboral mostra zero previsto e emprego indirecto por determinar, em duas linhas; económica mostra 125.7500 toneladas, 100000.00 MZN e 25.50 USD em divergência, em três linhas, com as respectivas fontes e períodos. Valores exclusivamente de teste, sem aplicação económica real. Nenhuma coordenada/geometria criada; hash de todas as fichas territoriais anteriores idêntico antes/depois. Recibo e hashes: `docs/investmoz/unification/quantitative-map-local-20261104.json`; inventário, capturas e DOM em `outputs/quantitative-map-20261104/`.
|
||||
|
||||
**ACT-02 permanece parcial.** Cobertura factual nacional/regional, séries comparáveis, oferta laboral, natureza das grandezas e metodologia quantitativa de potencial económico continuam pendentes conforme dados aplicáveis. A demonstração sintética não é estatística real, índice regional ou diagnóstico do portal remoto. Mantêm-se conferência remota e aceitação da equipa; V2-07/V2-10 não encerrados. Sem push, instalação, escrita activa, migração, publicação automática ou alteração de permissões/serviços.
|
||||
|
||||
Precisão adicional demonstrada: número JSON `1234567890123.456789` recuperado sem arredondamento. O valor é extraído como texto decimal do mesmo snapshot público carregado, sem o converter num float PHP nem misturar metadados e versão com uma leitura posterior. O teste dirigido de precisão e PHPStan passaram após esta correcção.
|
||||
|
||||
@@ -0,0 +1,183 @@
|
||||
{
|
||||
"date": "2026-11-04",
|
||||
"base_commit": "a9241ff2",
|
||||
"requirement": "ACT-20261029-02; V2-07/V2-10",
|
||||
"scope": "Primeiro recorte quantitativo: observações públicas existentes das zonas/parques do contexto do mapa; main local",
|
||||
"inventory_before_fixtures": {
|
||||
"database": "main_visual_20261104_test",
|
||||
"visible_zones": 8,
|
||||
"public_observations": [],
|
||||
"no_private_ciphertext_read": true
|
||||
},
|
||||
"source_reused": "InvestmentZoneOperationalDetails::publicData -> integral.observations; mesma selecção de divulgação da ficha, sem desencriptar preparação privada",
|
||||
"indicators": {
|
||||
"workforce": [
|
||||
"employment"
|
||||
],
|
||||
"economic_potential": [
|
||||
"investment",
|
||||
"production",
|
||||
"exports",
|
||||
"local_procurement"
|
||||
]
|
||||
},
|
||||
"contract": {
|
||||
"route": "GET /api/v1/investment/map",
|
||||
"new_field": "quantitative_context",
|
||||
"states": [
|
||||
"available",
|
||||
"unavailable",
|
||||
"not_included"
|
||||
],
|
||||
"client_network_error_state": "error",
|
||||
"zone_limit": 30,
|
||||
"record_limit": 200,
|
||||
"truncation_explicit": true,
|
||||
"scope": "Mesmo conjunto de zonas autorizado pelos filtros existentes de contexto; não alarga pesquisa nem representa valores das oportunidades. Filtros específicos que já excluem zonas devolvem not_included.",
|
||||
"metadata": [
|
||||
"id",
|
||||
"label",
|
||||
"description",
|
||||
"value",
|
||||
"unit",
|
||||
"period_start",
|
||||
"period_end",
|
||||
"scope",
|
||||
"population",
|
||||
"methodology",
|
||||
"source_reference",
|
||||
"source_date",
|
||||
"knowledge",
|
||||
"zone_id",
|
||||
"zone_name",
|
||||
"zone_version",
|
||||
"url",
|
||||
"theme",
|
||||
"missing_metadata",
|
||||
"phase_id",
|
||||
"plot_id"
|
||||
]
|
||||
},
|
||||
"behaviour": [
|
||||
"Valores numéricos conhecidos preservados como texto, incluindo zero e precisão decimal; sem conversão binária para somas.",
|
||||
"Valor desconhecido permanece null/Por determinar, mesmo perante quantidade contraditória numa fonte legada. Divergência conserva etiqueta.",
|
||||
"Unidade, período, base/população, âmbito, metodologia e fonte/data em cada linha; metadados em falta explicitados, sem os completar por inferência.",
|
||||
"Sem agregações, médias, câmbio, normalização ou comparação automática; emprego não equivale a disponibilidade laboral nem os montantes a índice de potencial.",
|
||||
"Contagens de oportunidades e de observações territoriais separadas. Sem geometrias ou posições criadas.",
|
||||
"Carregamento, erro, cadastro indisponível e âmbito não consultado distintos de resposta sem observações.",
|
||||
"Números JSON com casas decimais extraídos como texto decimal pelo PostgreSQL a partir do mesmo snapshot carregado; metadados/valor/versão não são emparelhados com uma leitura posterior."
|
||||
],
|
||||
"checks": {
|
||||
"database_tests": 11,
|
||||
"database_assertions": 99,
|
||||
"new_database_tests": 6,
|
||||
"existing_thematic_regression_tests": 5,
|
||||
"frontend_tests": 18,
|
||||
"new_frontend_tests": 3,
|
||||
"phpstan_errors": 0,
|
||||
"types": "passed",
|
||||
"pint": "passed",
|
||||
"lint": "passed",
|
||||
"qa_build": "passed; avisos de fonte opcional e dimensão de chunks"
|
||||
},
|
||||
"initial_attempts": "Nove dos dez casos BD passaram inicialmente; comparação da serialização JSON antes/depois do jsonb corrigida para comparar duas leituras reais do servidor. Fixture frontend repetia o rótulo de emprego numa linha de produção; rótulo corrigido. Sem alterar regras do produto para satisfazer estes testes. Logs BD iniciais conservados em quantitative-map-initial.*.",
|
||||
"visual": {
|
||||
"url": "http://127.0.0.2:18644/oportunidades?q=SYNTHETIC-QUANT-20261104&province_id=bfde2651-2b46-5d9c-a500-733fa55a8c5c&view=map",
|
||||
"database": "main_visual_20261104_test",
|
||||
"synthetic_zone_id": "b3349058-f120-4704-b34c-32ba7c1146c4",
|
||||
"synthetic_observations": 5,
|
||||
"workforce_rows": 2,
|
||||
"economic_rows": 3,
|
||||
"values_checked": [
|
||||
"0 pessoas (emprego previsto sintético)",
|
||||
"Por determinar (emprego indirecto sintético)",
|
||||
"125.7500 toneladas",
|
||||
"100000.00 MZN",
|
||||
"25.50 USD em divergência"
|
||||
],
|
||||
"coordinates_created": false,
|
||||
"geometry_created": false,
|
||||
"existing_zones_sha256": "8d7d6d778f826453556caa131def70747706ac338aac472959a2057811d4cc77"
|
||||
},
|
||||
"preservation": {
|
||||
"previous_artifact_hashes_verified_unchanged": 51,
|
||||
"accepted_full_database_tests_preserved_not_reexecuted": 1325,
|
||||
"previous_pending_items_preserved": true
|
||||
},
|
||||
"pending": [
|
||||
"O snapshot QA tinha oito fichas públicas e zero observações quantitativas divulgadas; demonstração não estabelece cobertura factual real nem diagnostica dados do remoto.",
|
||||
"Séries oficiais/regionais comparáveis, oferta laboral, natureza prevista/efectiva e metodologia de potencial quantitativo dependem dos dados aplicáveis, sem novos índices presumidos.",
|
||||
"Cobertura nacional, georreferenciação própria, conferência remota e aceitação da equipa continuam pendentes. ACT-02/V2-07/V2-10 não encerrados."
|
||||
],
|
||||
"safety": {
|
||||
"branch": "main local",
|
||||
"push": false,
|
||||
"active_database_changed": false,
|
||||
"migration": false,
|
||||
"permissions_changed": false,
|
||||
"automatic_publication": false,
|
||||
"service_restarts": false
|
||||
},
|
||||
"proofs": {
|
||||
"outputs/quantitative-map-20261104/source-inventory-before-fixtures.json": {
|
||||
"sha256": "b547bd8c34c09c7d779c8226e9a68ad7160d008bf9e2f68832c694022eb36597",
|
||||
"bytes": 142
|
||||
},
|
||||
"outputs/quantitative-map-20261104/prior-evidence.json": {
|
||||
"sha256": "49989437b12a348ca866404523e81d33fd695e0298acd2574ed579eb1872a915",
|
||||
"bytes": 6766
|
||||
},
|
||||
"outputs/quantitative-map-20261104/fixture-receipt.json": {
|
||||
"sha256": "2053ed900664fa65372bc4c0404da872b167213ca025ba88799af8221acfd46f",
|
||||
"bytes": 409
|
||||
},
|
||||
"outputs/quantitative-map-20261104/all-indicators.jpg": {
|
||||
"sha256": "9d1a455c1298f2c2579f37b354ab72aae32b5045c957a203b657cb6932d871da",
|
||||
"bytes": 217406
|
||||
},
|
||||
"outputs/quantitative-map-20261104/all-indicators.dom.txt": {
|
||||
"sha256": "c8e998ea7f4efdb1df6df57c07b014d86efdd4af9479ebea7bb52af964013285",
|
||||
"bytes": 23575
|
||||
},
|
||||
"outputs/quantitative-map-20261104/workforce-indicators.jpg": {
|
||||
"sha256": "e14ee34543d5482a5c288bd3bb0733b496d4aecefee12cd0f5d0e5d8aa6f1576",
|
||||
"bytes": 187352
|
||||
},
|
||||
"outputs/quantitative-map-20261104/workforce-indicators.dom.txt": {
|
||||
"sha256": "d659fd72bf7df061c7ff6542c0dee8e10ef74a4a7ba45ba1abb3adb0fc5a4d03",
|
||||
"bytes": 17344
|
||||
},
|
||||
"outputs/quantitative-map-20261104/economic-indicators.dom.txt": {
|
||||
"sha256": "11ebdf8284c36f20b2196f4bc4eada37c70c99d232c03a31ef54d673cfb8ddbb",
|
||||
"bytes": 19366
|
||||
},
|
||||
"outputs/main-db-qa-20261104/quantitative-map.log": {
|
||||
"sha256": "7914ad0e266ae90a884c5f3ffa783a7859d4e342a7a64fb6c96f90388be44f97",
|
||||
"bytes": 96
|
||||
},
|
||||
"outputs/main-db-qa-20261104/quantitative-map.junit.xml": {
|
||||
"sha256": "5d01befa688ec8edd9e3a58f9821f248fda5ec6c86318a02ada7b48c6b4b9494",
|
||||
"bytes": 4703
|
||||
},
|
||||
"outputs/main-db-qa-20261104/quantitative-map-frontend.log": {
|
||||
"sha256": "240847ad2e9d75e2d77bb3ea66e2814d18dbe7b85e79722da11306ec843b2941",
|
||||
"bytes": 2476
|
||||
},
|
||||
"outputs/main-db-qa-20261104/quantitative-map-phpstan.log": {
|
||||
"sha256": "1997495ca8f04c2ec09798d55607d1a2f85e3338b772f258666465bf658032d2",
|
||||
"bytes": 49
|
||||
},
|
||||
"outputs/main-db-qa-20261104/quantitative-map-types.log": {
|
||||
"sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
|
||||
"bytes": 0
|
||||
},
|
||||
"outputs/main-db-qa-20261104/quantitative-map-lint.log": {
|
||||
"sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
|
||||
"bytes": 0
|
||||
},
|
||||
"outputs/main-db-qa-20261104/quantitative-map-build.log": {
|
||||
"sha256": "6cefd15069e09c1bfd8b404842b719227ea7785e7908f07a69f66029fe4639e3",
|
||||
"bytes": 31606
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -9154,3 +9154,20 @@ Delta aprovado: sete testes de BD/77 asserções (cinco do mapa, incluindo o nov
|
||||
Conferência visual no Edge exclusivamente na QA `127.0.0.2:18644`/`main_visual_20261104_test`: contexto económico reutiliza a evidência de actividade já publicada da ficha 383, sintética e explicitamente identificada. Mão-de-obra usa uma única fixture nova, também sintética; escolha e fonte recuperadas após recarregar. Nenhuma coordenada/geometria criada; hash de todas as oportunidades anteriores idêntico antes/depois. Conferidos 36 artefactos dos três recibos anteriores, todos com SHA256 inalterado. Recibo: `docs/investmoz/unification/workforce-economic-map-local-20261104.json`; inventário, DOM, capturas e hashes em `outputs/workforce-economic-map-20261104/`.
|
||||
|
||||
**ACT-02 permanece parcial.** Cobertura factual laboral/económica, séries, unidades e metodologia quantitativa comparável, georreferenciação própria documentada e cobertura cartográfica continuam pendentes. A demonstração sintética não mede oferta laboral nem potencial regional real. Mantêm-se conferência remota e aceitação da equipa; V2-07/V2-10 não encerrados. Sem push, migração, publicação automática, escrita activa, alteração de permissões ou reinício de serviços.
|
||||
|
||||
|
||||
### 04/11/2026 — ACT-20261029-02: observações quantitativas públicas de emprego e contexto económico
|
||||
|
||||
Incremento na main local sobre `a9241ff2`. Inventário prévio: fichas de parques já conservam observações com valor, unidade, período, âmbito, população/base, metodologia, conhecimento e divulgação seleccionada. Reutilizada apenas a versão pública de `operational_details.public.integral.observations`, sem desencriptar preparação privada. Na cópia QA havia oito fichas públicas e zero observações quantitativas divulgadas; não foi declarada perda nem ausência no remoto. As 51 provas anteriores foram conferidas por SHA256 antes da ronda e novamente no fim, todas inalteradas; bateria geral aceite preservada sem repetição integral.
|
||||
|
||||
A API de mapa acrescenta `quantitative_context` a partir do mesmo conjunto de zonas já devolvido no contexto territorial: emprego para mão-de-obra; investimento, produção, exportações e compras locais para contexto económico. Reutiliza os filtros/autorização existentes, sem alargar a pesquisa de oportunidades nem transformar indicadores do parque em indicadores das oportunidades. Filtros específicos que já excluem esse conjunto devolvem âmbito não incluído, em vez de uma falsa medição vazia. Até 30 fichas e 200 observações; truncagem explícita. Geometrias, divulgação, fontes e versões anteriores conservadas.
|
||||
|
||||
O painel do mapa mostra cada valor com a sua unidade, período, âmbito, população/base, metodologia, fonte/data, versão pública e ligação à ficha. Zero documentado e valor desconhecido são distintos; precisão decimal conserva-se como texto. Divergências e metadados em falta permanecem visíveis. Emprego não é oferta de trabalhadores disponíveis; natureza prevista/efectiva depende da descrição da fonte. Não se somam populações ou períodos, não se convertem unidades/moedas e não se criam médias, índices ou previsões de retorno. Contagens de oportunidades e de observações territoriais têm âmbitos distintos. Erro, carregamento, cadastro indisponível, âmbito não consultado e ausência de observações são estados diferentes.
|
||||
|
||||
Verificação dirigida aprovada: 11 testes BD/99 asserções (seis novos e cinco do mapa temático existente), 18 testes frontend (três novos e 15 existentes), PHPStan sem erros, tipos, Pint, lint e compilação QA aprovados. Inclui zero/decimais, desconhecido/divergência, metadados incompletos, exclusão de preparação privada/ficha não publicada, preservação do envelope, província/distrito, limites de observações/fichas e âmbito excluído. Primeira tentativa teve uma expectativa inadequada sobre a serialização JSON antes da normalização jsonb; comparação corrigida para duas leituras efectivas do servidor. Fixture frontend com rótulo repetido corrigida; não se relaxaram regras do produto. Logs BD iniciais mantidos.
|
||||
|
||||
Conferência visual exclusivamente em `127.0.0.2:18644`/`main_visual_20261104_test`: uma ficha sintética nova com cinco observações. Camada laboral mostra zero previsto e emprego indirecto por determinar, em duas linhas; económica mostra 125.7500 toneladas, 100000.00 MZN e 25.50 USD em divergência, em três linhas, com as respectivas fontes e períodos. Valores exclusivamente de teste, sem aplicação económica real. Nenhuma coordenada/geometria criada; hash de todas as fichas territoriais anteriores idêntico antes/depois. Recibo e hashes: `docs/investmoz/unification/quantitative-map-local-20261104.json`; inventário, capturas e DOM em `outputs/quantitative-map-20261104/`.
|
||||
|
||||
**ACT-02 permanece parcial.** Cobertura factual nacional/regional, séries comparáveis, oferta laboral, natureza das grandezas e metodologia quantitativa de potencial económico continuam pendentes conforme dados aplicáveis. A demonstração sintética não é estatística real, índice regional ou diagnóstico do portal remoto. Mantêm-se conferência remota e aceitação da equipa; V2-07/V2-10 não encerrados. Sem push, instalação, escrita activa, migração, publicação automática ou alteração de permissões/serviços.
|
||||
|
||||
Precisão adicional demonstrada: número JSON `1234567890123.456789` recuperado sem arredondamento. O valor é extraído como texto decimal do mesmo snapshot público carregado, sem o converter num float PHP nem misturar metadados e versão com uma leitura posterior. O teste dirigido de precisão e PHPStan passaram após esta correcção.
|
||||
|
||||
@@ -3462,3 +3462,20 @@ Delta aprovado: sete testes de BD/77 asserções (cinco do mapa, incluindo o nov
|
||||
Conferência visual no Edge exclusivamente na QA `127.0.0.2:18644`/`main_visual_20261104_test`: contexto económico reutiliza a evidência de actividade já publicada da ficha 383, sintética e explicitamente identificada. Mão-de-obra usa uma única fixture nova, também sintética; escolha e fonte recuperadas após recarregar. Nenhuma coordenada/geometria criada; hash de todas as oportunidades anteriores idêntico antes/depois. Conferidos 36 artefactos dos três recibos anteriores, todos com SHA256 inalterado. Recibo: `docs/investmoz/unification/workforce-economic-map-local-20261104.json`; inventário, DOM, capturas e hashes em `outputs/workforce-economic-map-20261104/`.
|
||||
|
||||
**ACT-02 permanece parcial.** Cobertura factual laboral/económica, séries, unidades e metodologia quantitativa comparável, georreferenciação própria documentada e cobertura cartográfica continuam pendentes. A demonstração sintética não mede oferta laboral nem potencial regional real. Mantêm-se conferência remota e aceitação da equipa; V2-07/V2-10 não encerrados. Sem push, migração, publicação automática, escrita activa, alteração de permissões ou reinício de serviços.
|
||||
|
||||
|
||||
### 04/11/2026 — ACT-20261029-02: observações quantitativas públicas de emprego e contexto económico
|
||||
|
||||
Incremento na main local sobre `a9241ff2`. Inventário prévio: fichas de parques já conservam observações com valor, unidade, período, âmbito, população/base, metodologia, conhecimento e divulgação seleccionada. Reutilizada apenas a versão pública de `operational_details.public.integral.observations`, sem desencriptar preparação privada. Na cópia QA havia oito fichas públicas e zero observações quantitativas divulgadas; não foi declarada perda nem ausência no remoto. As 51 provas anteriores foram conferidas por SHA256 antes da ronda e novamente no fim, todas inalteradas; bateria geral aceite preservada sem repetição integral.
|
||||
|
||||
A API de mapa acrescenta `quantitative_context` a partir do mesmo conjunto de zonas já devolvido no contexto territorial: emprego para mão-de-obra; investimento, produção, exportações e compras locais para contexto económico. Reutiliza os filtros/autorização existentes, sem alargar a pesquisa de oportunidades nem transformar indicadores do parque em indicadores das oportunidades. Filtros específicos que já excluem esse conjunto devolvem âmbito não incluído, em vez de uma falsa medição vazia. Até 30 fichas e 200 observações; truncagem explícita. Geometrias, divulgação, fontes e versões anteriores conservadas.
|
||||
|
||||
O painel do mapa mostra cada valor com a sua unidade, período, âmbito, população/base, metodologia, fonte/data, versão pública e ligação à ficha. Zero documentado e valor desconhecido são distintos; precisão decimal conserva-se como texto. Divergências e metadados em falta permanecem visíveis. Emprego não é oferta de trabalhadores disponíveis; natureza prevista/efectiva depende da descrição da fonte. Não se somam populações ou períodos, não se convertem unidades/moedas e não se criam médias, índices ou previsões de retorno. Contagens de oportunidades e de observações territoriais têm âmbitos distintos. Erro, carregamento, cadastro indisponível, âmbito não consultado e ausência de observações são estados diferentes.
|
||||
|
||||
Verificação dirigida aprovada: 11 testes BD/99 asserções (seis novos e cinco do mapa temático existente), 18 testes frontend (três novos e 15 existentes), PHPStan sem erros, tipos, Pint, lint e compilação QA aprovados. Inclui zero/decimais, desconhecido/divergência, metadados incompletos, exclusão de preparação privada/ficha não publicada, preservação do envelope, província/distrito, limites de observações/fichas e âmbito excluído. Primeira tentativa teve uma expectativa inadequada sobre a serialização JSON antes da normalização jsonb; comparação corrigida para duas leituras efectivas do servidor. Fixture frontend com rótulo repetido corrigida; não se relaxaram regras do produto. Logs BD iniciais mantidos.
|
||||
|
||||
Conferência visual exclusivamente em `127.0.0.2:18644`/`main_visual_20261104_test`: uma ficha sintética nova com cinco observações. Camada laboral mostra zero previsto e emprego indirecto por determinar, em duas linhas; económica mostra 125.7500 toneladas, 100000.00 MZN e 25.50 USD em divergência, em três linhas, com as respectivas fontes e períodos. Valores exclusivamente de teste, sem aplicação económica real. Nenhuma coordenada/geometria criada; hash de todas as fichas territoriais anteriores idêntico antes/depois. Recibo e hashes: `docs/investmoz/unification/quantitative-map-local-20261104.json`; inventário, capturas e DOM em `outputs/quantitative-map-20261104/`.
|
||||
|
||||
**ACT-02 permanece parcial.** Cobertura factual nacional/regional, séries comparáveis, oferta laboral, natureza das grandezas e metodologia quantitativa de potencial económico continuam pendentes conforme dados aplicáveis. A demonstração sintética não é estatística real, índice regional ou diagnóstico do portal remoto. Mantêm-se conferência remota e aceitação da equipa; V2-07/V2-10 não encerrados. Sem push, instalação, escrita activa, migração, publicação automática ou alteração de permissões/serviços.
|
||||
|
||||
Precisão adicional demonstrada: número JSON `1234567890123.456789` recuperado sem arredondamento. O valor é extraído como texto decimal do mesmo snapshot público carregado, sem o converter num float PHP nem misturar metadados e versão com uma leitura posterior. O teste dirigido de precisão e PHPStan passaram após esta correcção.
|
||||
|
||||
@@ -0,0 +1,246 @@
|
||||
export type QuantitativeMapContext = {
|
||||
state: 'available' | 'unavailable' | 'not_included' | 'error';
|
||||
records: {
|
||||
id: string;
|
||||
zone_id: string;
|
||||
zone_name: string;
|
||||
zone_version: number;
|
||||
url: string;
|
||||
theme: 'workforce' | 'economic_potential';
|
||||
indicator: string;
|
||||
label?: string | null;
|
||||
description?: string | null;
|
||||
value: string | null;
|
||||
unit?: string | null;
|
||||
period_start?: string | null;
|
||||
period_end?: string | null;
|
||||
scope?: string | null;
|
||||
population?: string | null;
|
||||
methodology?: string | null;
|
||||
source_reference?: string | null;
|
||||
source_date?: string | null;
|
||||
knowledge: string;
|
||||
missing_metadata: string[];
|
||||
}[];
|
||||
zones_consulted?: number;
|
||||
zone_limit?: number;
|
||||
record_limit?: number;
|
||||
truncated: boolean;
|
||||
};
|
||||
const labels: Record<string, string> = {
|
||||
employment: 'Emprego',
|
||||
investment: 'Investimento',
|
||||
production: 'Produção',
|
||||
exports: 'Exportações',
|
||||
local_procurement: 'Compras locais',
|
||||
};
|
||||
const metadataLabels: Record<string, string> = {
|
||||
unit: 'unidade',
|
||||
period_start: 'início do período',
|
||||
period_end: 'fim do período',
|
||||
scope: 'âmbito',
|
||||
population: 'população/base',
|
||||
methodology: 'metodologia',
|
||||
source_reference: 'fonte',
|
||||
source_date: 'data da fonte',
|
||||
};
|
||||
const text = (value?: string | null) => value?.trim() || 'Não indicado';
|
||||
|
||||
export default function InvestmentMapIndicators({
|
||||
context,
|
||||
theme,
|
||||
}: {
|
||||
context: QuantitativeMapContext | null;
|
||||
theme: string;
|
||||
}) {
|
||||
if (!['all', 'workforce', 'economic_potential'].includes(theme))
|
||||
return null;
|
||||
const rows =
|
||||
context?.records.filter(
|
||||
(row) => theme === 'all' || row.theme === theme,
|
||||
) ?? [];
|
||||
return (
|
||||
<section
|
||||
className="space-y-2 rounded border p-3"
|
||||
aria-label="Indicadores quantitativos do contexto territorial"
|
||||
>
|
||||
<h3 className="font-semibold">
|
||||
Indicadores quantitativos do contexto territorial
|
||||
</h3>
|
||||
<p>
|
||||
Observações públicas das zonas e parques devolvidos como
|
||||
contexto territorial, independentes das evidências das
|
||||
oportunidades. Emprego não mede trabalhadores disponíveis;
|
||||
investimento, produção e exportações não constituem um índice de
|
||||
potencial ou garantia de retorno. A natureza prevista ou
|
||||
efectiva de cada valor depende da descrição da fonte, não é
|
||||
determinada automaticamente.
|
||||
</p>
|
||||
{!context ? (
|
||||
<p role="status">A carregar indicadores quantitativos…</p>
|
||||
) : context.state === 'error' ? (
|
||||
<p role="alert">
|
||||
Não foi possível consultar os indicadores. Pode repetir a
|
||||
consulta do mapa.
|
||||
</p>
|
||||
) : context.state === 'not_included' ? (
|
||||
<p>
|
||||
O conjunto de zonas não está incluído com estes filtros
|
||||
específicos de oportunidades. Não foi feita uma medição
|
||||
quantitativa neste âmbito.
|
||||
</p>
|
||||
) : context.state === 'unavailable' ? (
|
||||
<p>
|
||||
Observações quantitativas indisponíveis nesta versão do
|
||||
cadastro; os restantes dados do mapa mantêm-se.
|
||||
</p>
|
||||
) : (
|
||||
<>
|
||||
<p role="status">
|
||||
{rows.length}{' '}
|
||||
{rows.length === 1 ? 'observação' : 'observações'} nesta
|
||||
camada, em {context.zones_consulted}{' '}
|
||||
{context.zones_consulted === 1
|
||||
? 'ficha territorial consultada.'
|
||||
: 'fichas territoriais consultadas.'}
|
||||
A contagem não mede cobertura nacional.
|
||||
</p>
|
||||
{context.truncated && (
|
||||
<p role="note">
|
||||
Cobertura incompleta: consulta limitada a{' '}
|
||||
{context.zone_limit} fichas e {context.record_limit}{' '}
|
||||
observações. Refine a localização.
|
||||
</p>
|
||||
)}
|
||||
{rows.length === 0 ? (
|
||||
<p>
|
||||
Sem observações públicas destes indicadores na
|
||||
resposta. Valor desconhecido não equivale a zero.
|
||||
</p>
|
||||
) : (
|
||||
<details>
|
||||
<summary>
|
||||
Consultar valores, unidades e fontes (
|
||||
{rows.length})
|
||||
</summary>
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-left text-sm">
|
||||
<caption>
|
||||
Cada linha conserva o âmbito da fonte.
|
||||
Sem somas, conversões de unidade ou
|
||||
moeda, médias ou comparação automática
|
||||
entre períodos e populações.
|
||||
</caption>
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">
|
||||
Ficha e indicador
|
||||
</th>
|
||||
<th scope="col">Valor e unidade</th>
|
||||
<th scope="col">Período e base</th>
|
||||
<th scope="col">
|
||||
Fonte e metodologia
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{rows.map((row) => (
|
||||
<tr
|
||||
key={`${row.zone_id}:${row.id}`}
|
||||
className="border-t align-top"
|
||||
>
|
||||
<th scope="row" className="p-2">
|
||||
<a
|
||||
className="underline"
|
||||
href={row.url}
|
||||
>
|
||||
{row.zone_name}
|
||||
</a>
|
||||
<p>
|
||||
{labels[
|
||||
row.indicator
|
||||
] ?? row.indicator}
|
||||
: {text(row.label)}
|
||||
</p>
|
||||
<p>
|
||||
{text(row.description)}
|
||||
</p>
|
||||
<p>
|
||||
Versão pública{' '}
|
||||
{row.zone_version}
|
||||
</p>
|
||||
</th>
|
||||
<td className="p-2">
|
||||
<p>
|
||||
{row.value ??
|
||||
'Por determinar'}{' '}
|
||||
· {text(row.unit)}
|
||||
</p>
|
||||
<p>
|
||||
{row.knowledge ===
|
||||
'documented'
|
||||
? 'Documentado na fonte'
|
||||
: row.knowledge ===
|
||||
'disputed'
|
||||
? 'Em divergência'
|
||||
: 'Desconhecido'}
|
||||
</p>
|
||||
</td>
|
||||
<td className="p-2">
|
||||
<p>
|
||||
{text(row.period_start)}{' '}
|
||||
a {text(row.period_end)}
|
||||
</p>
|
||||
<p>
|
||||
Âmbito:{' '}
|
||||
{text(row.scope)}
|
||||
</p>
|
||||
<p>
|
||||
População/base:{' '}
|
||||
{text(row.population)}
|
||||
</p>
|
||||
</td>
|
||||
<td className="p-2">
|
||||
<p>
|
||||
Fonte:{' '}
|
||||
{text(
|
||||
row.source_reference,
|
||||
)}{' '}
|
||||
· Data:{' '}
|
||||
{text(row.source_date)}
|
||||
</p>
|
||||
<p>
|
||||
Metodologia:{' '}
|
||||
{text(row.methodology)}
|
||||
</p>
|
||||
{row.missing_metadata
|
||||
.length > 0 && (
|
||||
<p role="note">
|
||||
Informação em falta:{' '}
|
||||
{row.missing_metadata
|
||||
.map(
|
||||
(key) =>
|
||||
metadataLabels[
|
||||
key
|
||||
] ??
|
||||
key,
|
||||
)
|
||||
.join(', ')}
|
||||
. Não agregar ou
|
||||
comparar como se
|
||||
estivesse completa.
|
||||
</p>
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</details>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -1,4 +1,7 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import InvestmentMapIndicators, {
|
||||
type QuantitativeMapContext,
|
||||
} from './investment-map-indicators';
|
||||
import CatalogueInvestmentMap from './catalogue-investment-map';
|
||||
import type {
|
||||
InvestmentMapArea,
|
||||
@@ -51,12 +54,14 @@ export default function ThematicInvestmentMap({
|
||||
records,
|
||||
territoryId,
|
||||
truncated,
|
||||
quantitative = null,
|
||||
}: {
|
||||
points: InvestmentMapFeature[] | null;
|
||||
areas: InvestmentMapArea[];
|
||||
records: ThematicMapRecord[];
|
||||
territoryId?: string;
|
||||
truncated: boolean;
|
||||
quantitative?: QuantitativeMapContext | null;
|
||||
}) {
|
||||
const [theme, setTheme] = useState('all');
|
||||
useEffect(() => {
|
||||
@@ -130,8 +135,8 @@ export default function ThematicInvestmentMap({
|
||||
(proof) => proof.category === key,
|
||||
),
|
||||
).length,
|
||||
)}
|
||||
)
|
||||
)}{' '}
|
||||
de oportunidades)
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
@@ -140,7 +145,9 @@ export default function ThematicInvestmentMap({
|
||||
evidência pública seleccionada. Os marcadores localizam a
|
||||
oportunidade; não representam a posição exacta, capacidade
|
||||
ou disponibilidade do recurso ou da infra-estrutura. A
|
||||
pesquisa geral mantém os seus filtros e resultados.
|
||||
pesquisa geral mantém os seus filtros e resultados. As
|
||||
contagens desta secção são de oportunidades; os indicadores
|
||||
territoriais têm contagem própria.
|
||||
</p>
|
||||
<p>
|
||||
Mão-de-obra: a evidência pode descrever necessidades ou
|
||||
@@ -174,9 +181,9 @@ export default function ThematicInvestmentMap({
|
||||
</p>
|
||||
{selected.length === 0 && (
|
||||
<p>
|
||||
Sem evidência pública local para esta camada e
|
||||
estes filtros. Não foram criadas posições no
|
||||
mapa.
|
||||
Sem evidência pública local nas oportunidades
|
||||
desta camada e estes filtros. Não foram criadas
|
||||
posições no mapa.
|
||||
</p>
|
||||
)}
|
||||
{selected.length > 0 && (
|
||||
@@ -267,6 +274,7 @@ export default function ThematicInvestmentMap({
|
||||
</>
|
||||
)}
|
||||
</section>
|
||||
<InvestmentMapIndicators context={quantitative} theme={theme} />
|
||||
<CatalogueInvestmentMap
|
||||
points={visiblePoints}
|
||||
areas={visibleAreas}
|
||||
|
||||
@@ -18,6 +18,7 @@ import { useEffect, useRef, useState } from 'react';
|
||||
import Map, {
|
||||
type ThematicMapRecord,
|
||||
} from '@/components/thematic-investment-map';
|
||||
import type { QuantitativeMapContext } from '@/components/investment-map-indicators';
|
||||
import type { InvestmentMapArea } from '@/components/interactive-investment-map';
|
||||
import { type TerritoryOption } from '@/components/investment-territory';
|
||||
import { Frame, button, card, money, api } from './shared';
|
||||
@@ -183,6 +184,8 @@ export default function Catalogue({
|
||||
const [thematicRecords, setThematicRecords] = useState<ThematicMapRecord[]>(
|
||||
[],
|
||||
);
|
||||
const [quantitativeContext, setQuantitativeContext] =
|
||||
useState<QuantitativeMapContext | null>(null);
|
||||
const [areas, setAreas] = useState<InvestmentMapArea[]>([]);
|
||||
const [unlocated, setUnlocated] = useState<
|
||||
{ id: number; title: string; location: string; url: string }[]
|
||||
@@ -326,6 +329,7 @@ export default function Catalogue({
|
||||
setPoints(null);
|
||||
setAreas([]);
|
||||
setThematicRecords([]);
|
||||
setQuantitativeContext(null);
|
||||
setUnlocated([]);
|
||||
setMapContext([]);
|
||||
setTruncated(false);
|
||||
@@ -336,16 +340,29 @@ export default function Catalogue({
|
||||
setPoints(data.features);
|
||||
setAreas(data.areas ?? []);
|
||||
setThematicRecords(data.thematic_records ?? []);
|
||||
setQuantitativeContext(
|
||||
data.quantitative_context ?? {
|
||||
state: 'unavailable',
|
||||
records: [],
|
||||
truncated: false,
|
||||
},
|
||||
);
|
||||
setUnlocated(data.unlocated ?? []);
|
||||
setMapContext(data.territorial_context ?? []);
|
||||
setTruncated(Boolean(data.truncated));
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
if (active)
|
||||
if (active) {
|
||||
setQuantitativeContext({
|
||||
state: 'error',
|
||||
records: [],
|
||||
truncated: false,
|
||||
});
|
||||
setError(
|
||||
'Mapa indisponível. A lista e os filtros continuam disponíveis.',
|
||||
);
|
||||
}
|
||||
});
|
||||
return () => {
|
||||
active = false;
|
||||
@@ -655,6 +672,7 @@ export default function Catalogue({
|
||||
points={error ? [] : points}
|
||||
areas={areas}
|
||||
records={thematicRecords}
|
||||
quantitative={quantitativeContext}
|
||||
truncated={truncated}
|
||||
territoryId={
|
||||
String(
|
||||
|
||||
@@ -0,0 +1,121 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature;
|
||||
|
||||
use App\Models\InvestmentZone;
|
||||
use App\Services\InvestmentGeography;
|
||||
use App\Services\InvestmentMapIndicators;
|
||||
use App\Services\InvestmentZones;
|
||||
use App\Support\IsolatedQaDatabase;
|
||||
use Illuminate\Foundation\Testing\DatabaseTransactions;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Str;
|
||||
use Tests\TestCase;
|
||||
|
||||
class InvestmentMapIndicatorsTest extends TestCase
|
||||
{
|
||||
use DatabaseTransactions;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
$this->assertTrue(IsolatedQaDatabase::allowsSchemaChanges());
|
||||
}
|
||||
|
||||
private function observation(string $indicator = 'employment', ?string $value = '0'): array
|
||||
{
|
||||
return ['id' => (string) Str::uuid(), 'label' => 'SYNTHETIC QA source observation', 'indicator' => $indicator,
|
||||
'value' => $value, 'unit' => $indicator === 'employment' ? 'pessoas' : 'toneladas', 'period_start' => '2025-01-01', 'period_end' => '2025-12-31',
|
||||
'scope' => 'Synthetic park only', 'population' => 'Synthetic declared establishments', 'methodology' => 'Synthetic documentary register',
|
||||
'source_reference' => 'SYNTHETIC QA only, not official statistics', 'source_date' => '2026-01-15', 'knowledge' => 'documented'];
|
||||
}
|
||||
|
||||
private function zone(string $prefix, array $rows, ?string $territory = null): InvestmentZone
|
||||
{
|
||||
$base = app(InvestmentZones::class)->visible()->where('category', 'industrial_park')->firstOrFail();
|
||||
$zone = $base->replicate();
|
||||
$zone->forceFill(['code' => 'QA-'.Str::uuid(), 'name' => $prefix.' synthetic park '.Str::uuid(), 'primary_territory_id' => $territory,
|
||||
'editorial_draft' => 'PRIVATE-DRAFT-NOT-TO-BE-DECRYPTED',
|
||||
'operational_details' => ['schema_version' => 1, 'public' => ['integral' => ['observations' => $rows]], 'private_ciphertext' => 'PRIVATE-QUANTITATIVE-SOURCE']])->save();
|
||||
|
||||
return $zone;
|
||||
}
|
||||
|
||||
public function test_public_values_keep_zero_decimal_precision_units_sources_and_incomplete_metadata_without_private_fields(): void
|
||||
{
|
||||
$prefix = 'QUANT-'.Str::uuid();
|
||||
$rows = [$this->observation(), $this->observation('production', '12.3400'),
|
||||
[...$this->observation('investment', '100'), 'unit' => null], [...$this->observation(), 'knowledge' => 'unknown', 'value' => '999'],
|
||||
[...$this->observation('exports', '25.50'), 'knowledge' => 'disputed', 'private_note' => 'PRIVATE-NOTE']];
|
||||
$zone = $this->zone($prefix, $rows);
|
||||
$before = $zone->fresh()->getRawOriginal('operational_details');
|
||||
$r = $this->getJson('/api/v1/investment/map?q='.$prefix)->assertOk()->assertJsonPath('quantitative_context.state', 'available')
|
||||
->assertJsonCount(5, 'quantitative_context.records')->assertJsonPath('quantitative_context.records.0.value', '0')
|
||||
->assertJsonPath('quantitative_context.records.1.value', '12.3400')->assertJsonPath('quantitative_context.records.0.unit', 'pessoas')
|
||||
->assertJsonPath('quantitative_context.records.2.missing_metadata', ['unit'])->assertJsonPath('quantitative_context.records.3.value', null)
|
||||
->assertJsonPath('quantitative_context.records.4.knowledge', 'disputed')->assertJsonPath('quantitative_context.records.0.source_date', '2026-01-15');
|
||||
$this->assertStringNotContainsString('PRIVATE-', $r->getContent());
|
||||
$this->assertSame($before, $zone->fresh()->getRawOriginal('operational_details'));
|
||||
$this->assertArrayNotHasKey('total', $r->json('quantitative_context'));
|
||||
}
|
||||
|
||||
public function test_numeric_json_decimals_are_read_exactly_from_the_same_public_snapshot(): void
|
||||
{
|
||||
$prefix = 'QUANT-'.Str::uuid();
|
||||
$zone = $this->zone($prefix, [$this->observation('production', '1')]);
|
||||
DB::update("UPDATE investment_zones SET operational_details=jsonb_set(operational_details, '{public,integral,observations,0,value}', '1234567890123.456789'::jsonb) WHERE id=?", [$zone->id]);
|
||||
$this->getJson('/api/v1/investment/map?q='.$prefix)->assertOk()
|
||||
->assertJsonPath('quantitative_context.records.0.value', '1234567890123.456789');
|
||||
}
|
||||
|
||||
public function test_draft_and_unselected_private_observations_remain_unavailable_and_specific_opportunity_filters_are_explicit(): void
|
||||
{
|
||||
$prefix = 'QUANT-'.Str::uuid();
|
||||
$draft = $this->zone($prefix, [$this->observation()]);
|
||||
$draft->forceFill(['publication_status' => 'draft'])->save();
|
||||
$private = $this->zone($prefix, []);
|
||||
$private->forceFill(['operational_details' => ['private_ciphertext' => 'PRIVATE-SOURCE-WITH-QUANTITY']])->save();
|
||||
$this->getJson('/api/v1/investment/map?q='.$prefix)->assertOk()->assertJsonCount(0, 'quantitative_context.records');
|
||||
$this->getJson('/api/v1/investment/map?q='.$prefix.'§or=agronegocio')->assertOk()
|
||||
->assertJsonPath('quantitative_context.state', 'not_included')->assertJsonCount(0, 'quantitative_context.records');
|
||||
}
|
||||
|
||||
public function test_quantitative_context_uses_existing_province_and_district_scope_without_moving_any_source(): void
|
||||
{
|
||||
$prefix = 'QUANT-'.Str::uuid();
|
||||
$districts = app(InvestmentGeography::class)->publicReferences()->where('location_type', 'district')->get(['id', 'parent_id']);
|
||||
$district = $districts->first();
|
||||
$other = $districts->first(fn ($row) => $row->parent_id !== $district->parent_id);
|
||||
$zone = $this->zone($prefix, [$this->observation()], $district->id);
|
||||
$this->zone($prefix, [$this->observation('production', '9')], $other->id);
|
||||
foreach ([['province_id' => $district->parent_id], ['province_id' => $district->parent_id, 'district_id' => $district->id]] as $filters) {
|
||||
$this->getJson('/api/v1/investment/map?'.http_build_query(['q' => $prefix, ...$filters]))->assertOk()
|
||||
->assertJsonCount(1, 'quantitative_context.records')->assertJsonPath('quantitative_context.records.0.zone_id', $zone->id);
|
||||
}
|
||||
$this->assertSame($district->id, $zone->fresh()->primary_territory_id);
|
||||
}
|
||||
|
||||
public function test_observation_limit_is_explicit_and_values_from_other_indicators_are_not_reinterpreted(): void
|
||||
{
|
||||
$rows = [];
|
||||
for ($i = 0; $i < 201; $i++) {
|
||||
$rows[] = $this->observation('production', (string) $i);
|
||||
}
|
||||
$rows[] = $this->observation('emissions', '42');
|
||||
$zone = $this->zone('QUANT-'.Str::uuid(), $rows);
|
||||
$result = app(InvestmentMapIndicators::class)->forZones(collect([$zone]));
|
||||
$this->assertCount(200, $result['records']);
|
||||
$this->assertTrue($result['truncated']);
|
||||
$this->assertSame('economic_potential', $result['records'][0]['theme']);
|
||||
}
|
||||
|
||||
public function test_zone_limit_does_not_claim_complete_territorial_coverage(): void
|
||||
{
|
||||
$prefix = 'QUANT-'.Str::uuid();
|
||||
for ($i = 0; $i < 31; $i++) {
|
||||
$this->zone($prefix, []);
|
||||
}
|
||||
$this->getJson('/api/v1/investment/map?q='.$prefix)->assertOk()->assertJsonPath('quantitative_context.zones_consulted', 30)
|
||||
->assertJsonPath('quantitative_context.truncated', true)->assertJsonCount(30, 'territorial_context');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,148 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import fs from 'node:fs';
|
||||
import { compileFunction } from 'node:vm';
|
||||
import { createRequire } from 'node:module';
|
||||
import React from 'react';
|
||||
import { renderToStaticMarkup } from 'react-dom/server';
|
||||
import ts from 'typescript';
|
||||
const require = createRequire(import.meta.url);
|
||||
const source = fs.readFileSync(
|
||||
new URL(
|
||||
'../../resources/js/components/investment-map-indicators.tsx',
|
||||
import.meta.url,
|
||||
),
|
||||
'utf8',
|
||||
);
|
||||
const compiled = ts.transpileModule(source, {
|
||||
compilerOptions: {
|
||||
module: ts.ModuleKind.CommonJS,
|
||||
target: ts.ScriptTarget.ES2022,
|
||||
jsx: ts.JsxEmit.ReactJSX,
|
||||
},
|
||||
}).outputText;
|
||||
const module = { exports: {} };
|
||||
compileFunction(compiled, ['require', 'module', 'exports'])(
|
||||
require,
|
||||
module,
|
||||
module.exports,
|
||||
);
|
||||
const Component = module.exports.default;
|
||||
const row = {
|
||||
id: '1',
|
||||
zone_id: 'park-a',
|
||||
zone_name: 'Synthetic park',
|
||||
zone_version: 3,
|
||||
url: '/zonas/park-a',
|
||||
theme: 'workforce',
|
||||
indicator: 'employment',
|
||||
label: 'Synthetic declared jobs',
|
||||
value: '0',
|
||||
unit: 'pessoas',
|
||||
period_start: '2025-01-01',
|
||||
period_end: '2025-12-31',
|
||||
scope: 'Park A only',
|
||||
population: 'Declared establishments',
|
||||
methodology: 'Documentary register',
|
||||
source_reference: '<script>synthetic source</script>',
|
||||
source_date: '2026-01-15',
|
||||
knowledge: 'documented',
|
||||
missing_metadata: [],
|
||||
};
|
||||
const context = {
|
||||
state: 'available',
|
||||
records: [
|
||||
row,
|
||||
{
|
||||
...row,
|
||||
id: '2',
|
||||
theme: 'economic_potential',
|
||||
indicator: 'production',
|
||||
label: 'Synthetic production',
|
||||
value: '12.3400',
|
||||
unit: 'toneladas',
|
||||
period_start: '2024-01-01',
|
||||
period_end: '2024-12-31',
|
||||
},
|
||||
],
|
||||
zones_consulted: 1,
|
||||
zone_limit: 30,
|
||||
record_limit: 200,
|
||||
truncated: false,
|
||||
};
|
||||
const render = (data, theme = 'all') =>
|
||||
renderToStaticMarkup(
|
||||
React.createElement(Component, { context: data, theme }),
|
||||
);
|
||||
|
||||
test('quantitative observations keep zero, decimal precision, units, periods, scope, methodology and public source', () => {
|
||||
const html = render(context);
|
||||
for (const value of [
|
||||
'0',
|
||||
'12.3400',
|
||||
'pessoas',
|
||||
'toneladas',
|
||||
'2025-01-01',
|
||||
'2024-01-01',
|
||||
'Park A only',
|
||||
'Declared establishments',
|
||||
'Documentary register',
|
||||
'2026-01-15',
|
||||
'Versão pública 3',
|
||||
])
|
||||
assert.ok(html.includes(value), value);
|
||||
assert.match(html, /Sem somas, conversões de unidade ou moeda/);
|
||||
assert.match(html, /Emprego não mede trabalhadores disponíveis/);
|
||||
assert.match(html, /não constituem um índice de potencial/);
|
||||
assert.doesNotMatch(html, /<script>/);
|
||||
assert.match(html, /href="\/zonas\/park-a"/);
|
||||
});
|
||||
|
||||
test('themes separate labour and economic observations without inventing totals or losing metadata', () => {
|
||||
assert.match(render(context, 'workforce'), /pessoas/);
|
||||
assert.doesNotMatch(render(context, 'workforce'), /toneladas/);
|
||||
assert.match(render(context, 'economic_potential'), /12.3400/);
|
||||
assert.doesNotMatch(
|
||||
render(context, 'economic_potential'),
|
||||
/Synthetic declared jobs/,
|
||||
);
|
||||
assert.equal(render(context, 'resource'), '');
|
||||
});
|
||||
|
||||
test('unknown values, disputes, missing metadata, incomplete coverage and excluded scope remain explicit', () => {
|
||||
const html = render({
|
||||
...context,
|
||||
truncated: true,
|
||||
records: [
|
||||
{
|
||||
...row,
|
||||
value: null,
|
||||
unit: null,
|
||||
knowledge: 'unknown',
|
||||
missing_metadata: ['unit', 'population'],
|
||||
},
|
||||
{ ...row, id: '3', knowledge: 'disputed' },
|
||||
],
|
||||
});
|
||||
assert.match(html, /Por determinar/);
|
||||
assert.match(html, /Em divergência/);
|
||||
assert.match(html, /Informação em falta: unidade, população\/base/);
|
||||
assert.match(html, /limitada a 30 fichas e 200 observações/);
|
||||
assert.match(
|
||||
render({ ...context, records: [] }),
|
||||
/Valor desconhecido não equivale a zero/,
|
||||
);
|
||||
assert.match(
|
||||
render({ ...context, state: 'not_included' }),
|
||||
/Não foi feita uma medição quantitativa/,
|
||||
);
|
||||
assert.match(
|
||||
render({ ...context, state: 'unavailable' }),
|
||||
/indisponíveis nesta versão/,
|
||||
);
|
||||
assert.match(render(null), /A carregar indicadores/);
|
||||
assert.match(
|
||||
render({ ...context, state: 'error' }),
|
||||
/Não foi possível consultar os indicadores/,
|
||||
);
|
||||
});
|
||||
@@ -51,9 +51,11 @@ function setup() {
|
||||
];
|
||||
},
|
||||
}
|
||||
: name === './catalogue-investment-map'
|
||||
? { default: Map }
|
||||
: require(name),
|
||||
: name === './investment-map-indicators'
|
||||
? { default: () => null }
|
||||
: name === './catalogue-investment-map'
|
||||
? { default: Map }
|
||||
: require(name),
|
||||
module,
|
||||
module.exports,
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user