Compare commits

...
8 changed files with 232 additions and 6 deletions
@@ -19,7 +19,8 @@ final class EnsureScopedPortalAdministration
$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/economic-references/lot-01', 'management/economic-references/lot-01/*',
$catalogue = $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/*',
'management/balcao/atribuicoes', 'management/balcao/atribuicoes/*', 'management/balcao/servicos',
+1 -1
View File
@@ -20,7 +20,7 @@ class TerritoryGeometryImport
private function guard(User $actor, bool $write = false): void
{
abort_unless($actor->can('manage-portal'), 403);
abort_unless($actor->can('manage-portal') || app(ScopedPortalAdministration::class)->administrative($actor), 403);
abort_unless(config('geometry-import.enabled'), 503, 'Importação cartográfica não disponibilizada.');
if (! $write) {
return;
@@ -318,4 +318,10 @@ Situação comum observada: `publication_status=draft`, `validation_status=valid
### Referência da entrada única de gestão — QA, 22/10/2026
Nos requisitos existentes `MASTER-9f6dd1d9-0001`, `MASTER-9f6dd1d9-0003` e `MASTER-02f44baf-0002`, acrescentar como evidência delimitada a secção «Entrada única de gestão — preparação em QA, 22/10/2026» do manual de rastreabilidade e `tests/Feature/ContentManagementEntryTest.php`: quatro testes/99 asserções, ClamAV real e reversão transaccional em QA. Origem do objectivo e das regras operacionais: decisão expressa da Tempo; navegação, filtros e recuperação do upload são escolhas de implementação. Reutilizados biblioteca, armazenamento, perfis e histórico. Não altera os critérios de aceitação nem fecha os requisitos integrais: integração/contas reais e categorias não demonstradas conservam o seu estado. Geometrias permanecem separadas.
Nos requisitos existentes `MASTER-9f6dd1d9-0001`, `MASTER-9f6dd1d9-0003` e `MASTER-02f44baf-0002`, acrescentar como evidência delimitada a secção «Entrada única de gestão — preparação em QA, 22/10/2026» do manual de rastreabilidade e `tests/Feature/ContentManagementEntryTest.php`: quatro testes/99 asserções, ClamAV real e reversão transaccional em QA. Origem do objectivo e das regras operacionais: decisão expressa da Tempo; navegação, filtros e recuperação do upload são escolhas de implementação. Reutilizados biblioteca, armazenamento, perfis e histórico. Não altera os critérios de aceitação nem fecha os requisitos integrais: integração/contas reais e categorias não demonstradas conservam o seu estado. Geometrias permanecem separadas.
AUD-GEO-02/03: Recuperação da demarcação territorial — QA, 22/10/2026. 408/581 limites recuperados em QA; cadeia territorial guardada/reaberta no portal. Homologacao ainda sem reposicao; consultar a nova secao no manual existente. Mantidas 173 correspondencias pendentes e ausencia de geometrias proprias das localidades. Origem: pedido expresso da Tempo de recuperar funcionalidade; enquadramento e mensagens sao escolhas tecnicas. Nao encerra o criterio de reposicao no destino.
AUD-GEO-02/03 - 22/10/2026: Demarcação territorial reposta na homologação — 22/10/2026. 408 limites (11/133/264) importados pela conta real/MFA, com tres recibos auditados; backup/restauro de 171 tabelas conferido, 333 ficheiros verificados. Consulta do mapa demonstrada na oportunidade real #30, sem a alterar. Acessos previstos e originais privados preservados. Reposicao no destino concluida; 26+147 correspondencias e ausencia de poligonos proprios de localidades mantidas como lacunas reais. Ver fecho e guia no manual existente; sem repetir provas QA aceites.
@@ -2254,4 +2254,80 @@ Verificação nova: quatro testes, 99 asserções, com transacções revertidas
No navegador, login e MFA normais da conta sintética de equipa; entrada → legislação; entrada → nova oportunidade → guardar preparação incompleta → recarregar, com título recuperado e ficha QA 241. Não foi repetido o workflow integral de revisão/publicação de oportunidades já existente; a prova nova cobre a entrada e a gravação/recuperação. O ensaio documental completo foi HTTP transaccional, não uma prova visual de cada clique. A conta QA tem autorizações explicitamente sintéticas; não comprova as designações das contas reais. Não transportar fixtures, configurações QA ou concessões de acesso.
Provas locais: `outputs/content-management-20261022/entrada-final.png`, `build-final.log`, e `tests/Feature/ContentManagementEntryTest.php`. Estado: candidata em branch `codex/content-management-entry-QA`, ainda sem integração ou instalação na homologação. Não declarar encerrada a autonomia operacional de todas as contas/categorias apenas por esta entrada. Não houve migration, publicação remota, alteração de papéis activos ou activação fiscal.
Provas locais: `outputs/content-management-20261022/entrada-final.png`, `build-final.log`, e `tests/Feature/ContentManagementEntryTest.php`. Estado: candidata em branch `codex/content-management-entry-QA`, ainda sem integração ou instalação na homologação. Não declarar encerrada a autonomia operacional de todas as contas/categorias apenas por esta entrada. Não houve migration, publicação remota, alteração de papéis activos ou activação fiscal.
## Recuperação da demarcação territorial — QA, 22/10/2026
Retomada expressa da Tempo como recuperação de funcionalidade anteriormente demonstrada, nos requisitos existentes AUD-GEO-02/03 e na experiência de cadastro. Não é criação de outra cartografia. O incremento de divulgação promocional permanece na sua branch independente.
**Causa comprovada:** leitura SSH autenticada, em transacção READ ONLY, da release `8286d750`/base `teste_investidor`: zero linhas em `geo_geometries`, incluindo eliminadas. Cadastro preservado: 11 províncias, 148 distritos, 437 postos e 1324 localidades. Directório dos originais privados ausente; importador desabilitado. Serviço de consulta, controlador e correspondências iguais por SHA256 ao código QA. Hash do cadastro (IDs, nomes, pais, nível, origem/códigos): `9de4181c42055bc6ef11b56333b64dd463d0ebe16bb2d1ea3238fc79e69024f7`, igual no QA e destino. Não se confirmou recusa de acesso nem perda de associação: faltam os próprios dados.
**Histórico:** as provas de 09–10/10 em `docs/investmoz/v2/auditoria-portal/correspondencias-cartograficas.md` registam 361→393→408 correspondências em bases QA anteriores. Os originais e recibos privados permanecem disponíveis. Os transportes de 19–20/10 deste manual excluíram expressamente geometrias; a disponibilização Git também exclui dados e ficheiros privados (`deploy/homologacao/README.md`). Em 22/10 a leitura de Cabo Delgado já encontrara zero linhas; esta passagem conferiu a tabela inteira. Prova-se uma diferença de composição entre QA cartográfico e homologação. Não foi localizado recibo da instalação anterior destes limites na mesma base; não se atribui data a uma suposta eliminação nem se descarta a observação anterior no mesmo endereço.
**Recuperação executada em QA:** o clone actual também tinha zero geometrias. Reutilizados seis originais INE/UNOCHA de 07/06/2019 (três GeoJSON e três metadados), hashes preservados e correspondências v2 existentes. O serviço `TerritoryGeometryImport` inseriu 11 províncias, 133 distritos e 264 postos, internos, com recibos e auditoria. Todos válidos/SRID4326. Nos exemplos, a consulta pública continua sem devolver limites internos. ClamAV real, assinaturas de 22/10: três originais limpos. ADM3, 26773796 bytes, excedeu o stream de 25 MiB e passou pelo scanner de ficheiro GIS com orçamento de 40 MiB nesse processo; tentativa inconclusiva preservada. Sem alterar limites globais ou configuração do daemon.
O mapa enquadra o limite seleccionado; se não existir, enquadra o nível superior disponível mais próximo e informa que é contexto. A lista distingue selecção de contexto superior. Fonte, edição e actualidade jurídica indeterminada preservadas. Limite administrativo não representa parcela nem autorização de investimento.
**Prova de interface:** oportunidade sintética QA268 criada no portal. Cabo Delgado → ANCUABE → posto ANCUABE → ANCUABE-SEDE; limites e aproximação progressiva nos três primeiros níveis. Recurso de contexto/principal guardado; após recarregar e abrir “Ver / editar / restaurar”, os quatro níveis foram recuperados. A localidade sem limite próprio apresenta aviso dos níveis superiores. A fixture QA255 anterior tinha versão fechada/payload sintético mínimo incompatível com gravação do contrato; não foi usada como prova de persistência nem corrigida silenciosamente. Uma navegação durante a recompilação QA conservava referência a asset removido; recarregar resolveu o carregamento, sem alteração no destino.
**Cobertura:** 408/581 elementos do exemplar aceite: 11/11 províncias, 133/159 distritos, 264/411 postos. Mantêm-se 26+147 correspondências pendentes, com nomes nas prévias e no documento de correspondências existente. O denominador do cadastro é diferente (148 distritos, 437 postos). Este exemplar não contém geometrias próprias de localidades ou parcelas; não atribuir limites superiores às 1324 localidades.
**Reposição preparada, não aplicada na homologação:** `outputs/investmoz-territory-recovery-20261022/demarcation/retained-geometry-restoration.zip`, SHA256 `53a5d8aa2be57603c928279cbd14561741dd2ec4b44b138fbc785380de6f951d`, contém seis originais e manifesto. Não contém dump de substituição, actores QA ou limites sintéticos. Procedimento existente:
1. Rever e integrar pelo Git o ajuste visual; conferir revisão efectivamente servida.
2. Colocar originais no armazenamento privado partilhado, conservando hashes. Executar scanner actual ligado aos originais. Conferir backup/restauro e prova de activação da revisão/alvo, como exige o importador já existente; disponibilizar a configuração do importador.
3. Gestor já autorizado, com MFA, abre `/management/territory-geometry-import`: prévia e aplicação de províncias, depois distritos, depois postos, com motivo/confirmar. Reconciliar prévia fresca no destino; não executar hashes antigos do QA. Recibos e reversão dos lotes novos existentes preservados.
4. Conferir a cadeia territorial no formulário do destino e a recuperação após guardar, mantendo lacunas explícitas.
O staging dos originais, backup/restauro e configuração inicial ainda exigem operação técnica. Depois da reposição, seleccionar, guardar e recuperar a localização/mapa usam a interface. Esta passagem só leu o servidor remoto: sem alterar dados, permissões, camadas ou configuração. Não declarar a reposição no destino concluída.
Origem das restrições: decisões expressas da Tempo e requisitos anteriores do importador para preservação/backup e autenticação. Enquadramento e mensagens são escolhas de implementação, sem novo papel ou aprovador. Fonte oficial aceite reutilizada sem exigir nova validação da credibilidade.
Provas na pasta desta recuperação: inventário dos seis ficheiros, prévias/recibos por nível, `verification.json`, `homologation-readonly.json` e capturas. TypeScript, build QA e sintaxe PHP verificados; provas editoriais anteriores não repetidas.
### Ajuste de acesso encontrado na disponibilização — 22/10/2026
A conta real de Superutilizador encontrou HTTP403 no importador: o middleware não incluía a cartografia no âmbito administrativo já aprovado e o serviço exigia exclusivamente o gate legado `manage-portal`. Corrigidos ambos para reutilizar `ScopedPortalAdministration`, com MFA de sessão e as exclusões existentes de Auditor. Não foram alteradas atribuições, papéis, contas ou o gate global. Origem do bloqueio: escolha antiga de implementação, não decisão organizacional da Tempo. Três testes transaccionais novos/15 asserções em QA: Superutilizador com MFA chega à validação de fonte/aplicação; sem MFA e conta comum são recusados; nenhum grant legado criado. Provas QA cartográficas aceites reutilizadas sem repetição.
## Demarcação territorial reposta na homologação — 22/10/2026
Pedido expresso da Tempo executado. Candidata `15d82611` revista e integrada em `73c046e1`; ajuste de acesso `95d4cf44`, testado em QA, integrado em `30aed7cd`. A revisão `30aed7cd12ef65f797191467232087643c396751` foi instalada pelo processo Git existente e confirmada em `/build/revision.json?demarcation=30aed7cd`. O transporte de dados foi executado separadamente do Git.
### Dados, backup e importação
Seis originais INE/UNOCHA de 07/06/2019 transportados para o armazenamento privado partilhado: hashes iguais ao manifesto aceite, ficheiros 0640 e directório 0750, fora da árvore pública. Análise real dos seis pelo ClamAV 1.4.3, assinaturas de 22/10/2026, resultado limpo. Utilizado o protocolo local FILDES do daemon existente, sem instalar scanner, alargar permissões dos originais ou alterar limites globais: [documentação oficial ClamAV](https://docs.clamav.net/manual/Usage/ClamdProtocol.html).
Backup completo de `teste_investidor`, snapshot exportado consistente, restaurado em `127.0.0.1:55441/demarcation_hml_restore_20261022_complete_test`. As 171 tabelas coincidem integralmente por contagem e checksum canónico; arquivo dos 333 ficheiros verificado por conteúdo/hash. Recibo remoto em `/var/backups/investmoz-demarcation-20261022-complete/backup-receipt.json`, dump SHA256 `f80551de827e954a35b66ee4c8f2fcc559a4c88b48b3dc799c33b74907079c9b`. A comparação inicial revelou diferenças de ordenação Linux/Windows e a visão RLS reduzida da conta da aplicação; a comparação final usou ordenação canónica e a mesma visão completa do backup, preservando também os registos fiscais. Não se desactivou RLS.
A conta real `nala@tempo.co.mz` (ID15), com login e MFA normais renovados pelo titular, abriu as prévias frescas e aplicou os três níveis pela interface `/management/territory-geometry-import`:
| Nível | Criados | Pendentes | Lote auditado |
| --- | ---: | ---: | --- |
| Províncias | 11 | 0 | ec2c7c27-e4a3-466e-91a4-60aba939d3a0 |
| Distritos | 133 | 26 | 142667c7-7309-4461-8d0a-fd00cb421577 |
| Postos administrativos | 264 | 147 | b6d5a7e2-2f44-4bd6-9634-d97ee58ab718 |
Total: **408 limites internos**, válidos, não vazios, SRID4326; três recibos e três eventos `geometry_import_applied` ligados ao actor real. As geometrias foram repostas para consulta autorizada no cadastro; não foram publicadas camadas internas. Reversão existente disponível por lote, preservando originais, histórico e reutilizações e recusando alterações posteriores. Não se executou reversão no destino.
### Delta e acessos conferidos
Na oportunidade real #30, «Centro de Emergência Turística e Medicina de Mergulho de Tofo», a sessão real abriu Recursos → Ver recurso. O mapa desenha o limite da **província de Inhambane**, com fonte/edição e identificação de território seleccionado. Não é um polígono próprio de Tofo: distrito/posto/localidade continuam por indicar nessa ficha, como antes. A oportunidade, o recurso e a publicação não foram gravados nem alterados. Captura: `hml-demarcacao-tofo.png`. A prova QA aceite de guardar/reabrir quatro níveis foi reutilizada, sem repetição.
Consultas de controlo no destino recuperaram 1/2/3 limites na cadeia Cabo Delgado → distrito Ancuabe → posto Ancuabe. A localidade Ancuabe-Sede continua sem limite próprio e com três limites ancestrais de contexto. O cadastro territorial conserva exactamente o hash anterior `9de4181c42055bc6ef11b56333b64dd463d0ebe16bb2d1ea3238fc79e69024f7`.
Conferidos os acessos das contas existentes: IDs15 e16 mantêm a designação aprovada de Superutilizador; ID15 mantém Promotor e Validador. Não foram criadas contas ou atribuições. O endpoint do cadastro aceita os perfis já autorizados; a demonstração visual autenticada foi com ID15. A conferência das atribuições de ID16 foi de política, sem simular outro login. Quinze contas sem o perfil correspondente não satisfazem essa autorização. Pedido HTTP sem sessão ao endpoint privado devolveu 401; API pública devolveu zero limites internos; tentativa de obter o original pelo caminho público devolveu 403. Originais privados e regras de acesso preservados. Contas ainda não criadas não bloquearam a reposição.
O importador tinha um bloqueio legado de implementação: exigia `manage-portal` e não reconhecia a administração já aprovada. Corrigido por reutilização de `ScopedPortalAdministration`, com MFA de sessão e exclusões de Auditor existentes. Três testes novos/15 asserções passaram; não foi criado novo papel, aprovador ou gate global. Não houve repetição das provas QA cartográficas aceites.
Após a importação, **166/171 tabelas integralmente iguais** ao backup. Apenas `geo_geometries` (0→408), `geo_write_receipts` (0→3), `audit_events` (+6), `cache` e `sessions` diferem. Oportunidades, documentos, cadastro territorial, atribuições e par de Superutilizadores preservados integralmente.
### Guia curto e estado final
Entrar em [Gestão de conteúdos e oportunidades](https://teste-investidor.tempo.co.mz/gestao-conteudos) → Gerir oportunidades e histórico → abrir a ficha → Imagens, documentos e localizações → abrir ou preparar a localização. Seleccionar a hierarquia documentada; o mapa enquadra o limite disponível. Se faltar geometria própria, a mensagem distingue o contexto superior. Guardar o recurso e reabrir pela mesma lista recupera a preparação; não é necessário comando ou Codex para a consulta ou cadastro corrente.
**Reposição concluída neste recorte.** Mantidas as 26 correspondências de distritos e 147 de postos pendentes, com nomes nos documentos/prévias existentes. O exemplar não contém polígonos próprios das localidades ou parcelas. Essas lacunas não bloqueiam os 408 limites recuperados. A data de eventual desaparecimento anterior no mesmo endereço continua não determinada. A divulgação promocional mantém o seu incremento separado; este fecho não a declara integrada.
Provas: `homologation-restored-readonly.json`, `staging-receipt.json`, `preservation-receipt.json`, `http-access-verification.json`, captura e recibos locais na pasta desta recuperação; backup/restauro e prova de activação em caminhos privados. Uma interrupção do transporte local foi resolvida por escrita atómica da mensagem, antes de qualquer importação. Não ficaram passos técnicos pendentes para esta reposição; futuras importações usam as salvaguardas de backup/MFA já existentes.
@@ -19,6 +19,8 @@ export type InvestmentMapFeature = {
};
};
export type InvestmentMapArea = {
selected_territory?: boolean;
location_type?: string;
purpose?: string;
geometry: GeoJsonObject;
title: string;
@@ -74,6 +76,13 @@ export default function InteractiveInvestmentMap({
.addTo(instance);
L.control.scale({ imperial: false }).addTo(instance);
const bounds: [number, number][] = [];
const hasSelectedBoundary = areas.some((area) => area.selected_territory);
const administrativeLevel: Record<string, number> = {
province: 1, district: 2, admin_post: 3, locality: 4,
};
const closestContextLevel = contextOnly
? Math.max(0, ...areas.map((area) => administrativeLevel[area.location_type ?? ''] ?? 0))
: 0;
for (const area of areas) {
const label = document.createElement('p');
label.textContent = `${area.title} · ${area.purpose === 'administrative_boundary' ? 'Limite administrativo da edição indicada; não representa um local de investimento' : 'Polígono cadastrado, limites oficiais por verificar'} · Fonte: ${area.source_reference ?? 'Não indicada'} · Data: ${area.source_date ?? 'Não documentada'}`;
@@ -92,7 +101,10 @@ export default function InteractiveInvestmentMap({
.bindPopup(label)
.addTo(instance);
const extent = layer.getBounds();
if (extent.isValid())
const focusBoundary = hasSelectedBoundary
? area.selected_territory
: !closestContextLevel || administrativeLevel[area.location_type ?? ''] === closestContextLevel;
if (extent.isValid() && focusBoundary)
bounds.push(
[extent.getSouth(), extent.getWest()],
[extent.getNorth(), extent.getEast()],
@@ -71,6 +71,12 @@ export default function TerritoryGeometryPreview({
? 'Limite administrativo disponível para este território.'
: 'Sem limite administrativo acessível para este território neste perfil.'}
</p>
{!context.selected_geometry_available && context.areas.length > 0 && (
<p role="note">
O mapa mostra apenas limites dos níveis superiores como contexto.
Estes limites não representam o território seleccionado.
</p>
)}
<p>
{context.context_geometry_count} limites disponíveis na
hierarquia selecionada. São contexto administrativo, não
@@ -93,7 +99,7 @@ export default function TerritoryGeometryPreview({
<ul>
{context.areas.map((area, index) => (
<li key={index}>
{area.title} · Fonte:{' '}
{area.title} · {area.selected_territory ? 'Território seleccionado' : 'Contexto de nível superior'} · Fonte:{' '}
{area.source_reference ?? 'Não indicada'} ·
Edição: {area.source_date ?? 'Não indicada'}
</li>
@@ -0,0 +1,61 @@
<?php
namespace Tests\Feature;
use App\Models\User;
use App\Services\TerritoryGeometryImport;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Illuminate\Support\Facades\Crypt;
use Illuminate\Support\Facades\DB;
use Symfony\Component\HttpKernel\Exception\HttpException;
use Tests\TestCase;
class TerritoryGeometryApprovedAdministrationTest extends TestCase
{
use DatabaseTransactions;
public function test_existing_superuser_with_completed_mfa_reaches_importer_without_legacy_manager_grant(): void
{
$actor = $this->holder();
$this->actingAs($actor)->withSession(['portal.mfa_actor' => $actor->id, 'portal.mfa_at' => time(), 'portal.access_revision' => (int) $actor->portal_access_revision]);
$this->assertFalse($actor->can('manage-portal'));
// A missing exemplar must produce the source error, not deny the approved actor.
try {
app(TerritoryGeometryImport::class)->preview($actor, 1);
$this->fail('The deliberately absent source must be reported.');
} catch (HttpException $exception) {
$this->assertSame(422, $exception->getStatusCode());
}
$this->getJson('/management/territory-geometry-import')->assertStatus(422);
$this->postJson('/management/territory-geometry-import', [])->assertStatus(422);
$this->assertFalse($actor->can('manage-portal'));
}
public function test_superuser_without_mfa_session_cannot_import(): void
{
$actor = $this->holder();
$this->actingAs($actor)->getJson('/management/territory-geometry-import')->assertForbidden();
$this->postJson('/management/territory-geometry-import', [])->assertForbidden();
}
public function test_ordinary_account_does_not_gain_cartographic_administration(): void
{
$this->holder();
$actor = User::factory()->create();
$this->actingAs($actor)->getJson('/management/territory-geometry-import')->assertForbidden();
$this->postJson('/management/territory-geometry-import', [])->assertForbidden();
}
private function holder(): User
{
$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(['portal.profile_access_enabled' => true, 'portal.managers' => [], 'geometry-import.enabled' => true,
'geometry-import.directory' => storage_path('app/private/nonexistent-demarcation-source')]);
$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' => $actor->id, 'second_user_id' => $other->id, 'approved_by' => $other->id]);
return $actor;
}
}
@@ -0,0 +1,64 @@
<?php
// Rehearse recovery with the existing importer. Never accepts an active database.
require dirname(__DIR__).'/vendor/autoload.php';
$app = require dirname(__DIR__).'/bootstrap/app.php';
$app->make(Illuminate\Contracts\Console\Kernel::class)->bootstrap();
$target = Illuminate\Support\Facades\DB::selectOne('select current_database() as database,inet_server_port() as port');
throw_unless($target->database === 'simulator_fiscal_delta_20261021_test' && (int) $target->port === 55441
&& config('database.connections.pgsql.host') === '127.0.0.1', RuntimeException::class, 'Isolated QA only.');
$app->detectEnvironment(fn () => 'testing');
config(['geometry-import.enabled' => true, 'portal.managers' => ['qa-content-promoter@example.test'],
'services.malware.scanner' => 'clamd', 'services.malware.socket' => null,
'services.malware.host' => '127.0.0.1', 'services.malware.port' => 3310]);
$actor = App\Models\User::where('email', 'qa-content-promoter@example.test')->firstOrFail();
$directory = config('geometry-import.directory');
$out = storage_path('app/private/territory-demarcation-recovery');
Illuminate\Support\Facades\File::ensureDirectoryExists($out, 0700);
$manifest = [];
foreach (glob($directory.'/moz-adm*-ine-*') as $file) {
$manifest[] = ['file' => basename($file), 'bytes' => filesize($file), 'sha256' => hash_file('sha256', $file)];
}
file_put_contents($out.'/source-inventory.json', json_encode($manifest, JSON_PRETTY_PRINT|JSON_THROW_ON_ERROR));
$scanner = app(App\Services\Malware\MalwareScanner::class);
$health = $scanner->health();
throw_unless($health['available'], RuntimeException::class, 'Current scanner unavailable.');
$scans = [];
foreach ([1, 2, 3] as $level) {
$file = $directory.'/moz-adm'.$level.'-ine-20190607.geojson';
// Scope the stream budget to this retained GIS original in this process only.
config(['services.malware.max_file_bytes' => filesize($file), 'services.malware.max_stream_bytes' => filesize($file)]);
$scan = $scanner->scan($file, filesize($file), storage_path('app/private'));
$scan['signatures_at'] = $scan['signatures_at']?->format(DATE_ATOM);
if ($level === 3 && $scan['reason_code'] === 'scanner_decompression_limit') {
$binary = getenv('GIS_CLAMSCAN_BINARY');
$database = getenv('GIS_CLAMSCAN_DATABASE');
throw_unless(is_file($binary ?: '') && is_dir($database ?: ''), RuntimeException::class, 'GIS scanner paths required.');
$process = new Symfony\Component\Process\Process([$binary, '--database='.$database, '--max-filesize=40M', '--max-scansize=100M', '--no-summary', $file]);
$process->setTimeout(120)->mustRun();
throw_unless(str_ends_with(trim($process->getOutput()), ': OK'), RuntimeException::class, 'GIS original scan incomplete.');
$version = new Symfony\Component\Process\Process([$binary, '--database='.$database, '--version']);
$version->mustRun();
$parts = explode('/', trim($version->getOutput()));
$signatureTime = strtotime(end($parts));
throw_unless($signatureTime && time() - $signatureTime < 48 * 3600, RuntimeException::class, 'Current GIS signatures required.');
$scan = ['result' => 'clean', 'engine' => 'ClamAV', 'engine_version' => trim($version->getOutput()),
'signatures_at' => gmdate(DATE_ATOM, $signatureTime), 'scope' => 'retained GIS original only',
'original_sha256' => hash_file('sha256', $file), 'stream_attempt' => $scan, 'exit_code' => $process->getExitCode()];
}
$scans[] = ['file' => basename($file), 'sha256' => hash_file('sha256', $file), 'scan' => $scan];
echo json_encode(['level' => $level, 'scan' => $scan]).PHP_EOL;
throw_unless($scan['result'] === 'clean', RuntimeException::class, 'Original scan incomplete.');
}
file_put_contents($directory.'/geometry-source-scans.json', json_encode(['captured_at' => gmdate('c'), 'files' => $scans], JSON_PRETTY_PRINT|JSON_THROW_ON_ERROR));
$service = app(App\Services\TerritoryGeometryImport::class);
foreach ([1, 2, 3] as $level) {
$plan = $service->preview($actor, $level);
file_put_contents($out.'/preview-'.$level.'.json', json_encode($plan, JSON_PRETTY_PRINT|JSON_THROW_ON_ERROR));
echo json_encode(['level' => $level, 'resolved' => $plan['resolved'], 'pending' => $plan['pending']]).PHP_EOL;
if (! in_array('--apply', $argv, true)) break; // Lower levels require imported parent correspondences.
$result = $service->apply($actor, ['level' => $level, 'hash' => $plan['hash'], 'idempotency_key' => (string) Illuminate\Support\Str::uuid(),
'reason' => 'Recuperação QA da demarcação anteriormente demonstrada, com originais e correspondências aceites.', 'confirm' => true]);
file_put_contents($out.'/result-'.$level.'.json', json_encode($result, JSON_PRETTY_PRINT|JSON_THROW_ON_ERROR));
echo json_encode(['level' => $level, 'inserted' => $result['inserted'], 'reused' => $result['reused']]).PHP_EOL;
}