diff --git a/AGENTS.md b/AGENTS.md
new file mode 100644
index 00000000..16ff3deb
--- /dev/null
+++ b/AGENTS.md
@@ -0,0 +1,10 @@
+# Regras do acervo documental InvestMoz
+
+- O responsável define o acervo e a finalidade. Inventariar todos os ficheiros e anexos acessíveis antes de seleccionar documentos para consulta; não omitir por não serem usados por uma funcionalidade.
+- Manter três estados independentes: disponibilidade para consulta, situação jurídica/documental e utilização numa regra ou cálculo. Importação/publicação nunca activa um cálculo fiscal.
+- Não usar associação a módulo, requisito ou artigo como pré-condição para cadastrar ou consultar um documento autorizado. Relações sem citação verificada ficam pendentes e visivelmente identificadas.
+- Respeitar a autorização de divulgação registada e o seu âmbito. Só bloquear por impedimento concreto (por exemplo malware, dados pessoais/confidenciais ou restrição efectiva de divulgação), registado por ficheiro. Não atribuir decisões a gestores inexistentes nem contornar autenticação.
+- Importações em lote devem ter pré-visualização, cálculo de hash, detecção de duplicados por bytes e conflitos de identidade/versão, escrita privada, análise antimalware e execução idempotente. Nunca publicar automaticamente um lote.
+- Preservar o ficheiro original, a proveniência, o identificador exacto, a edição/data, o hash e o histórico. Não marcar autenticidade, vigência, direitos ou scan como confirmados sem evidência.
+- Para fontes fiscais, inventariar também as fontes não analisadas e os diplomas/artigos sem correspondência documental. Parâmetros ausentes ou ambiguidades permanecem indeterminados; não usar zero nem a taxa mais recente como substituto.
+- Antes de aplicar mudanças em dados activos, confirmar backup/restauro e a acção autenticada que continua necessária. Testes e migrações devem usar exclusivamente uma base QA isolada com guarda contra a base activa.
diff --git a/app/Console/Commands/ImportDocumentBatch.php b/app/Console/Commands/ImportDocumentBatch.php
new file mode 100644
index 00000000..fbed1729
--- /dev/null
+++ b/app/Console/Commands/ImportDocumentBatch.php
@@ -0,0 +1,276 @@
+argument('manifest'));
+ $stagingReal = realpath($staging);
+ if (! $manifestPath || ! $stagingReal || ! str_starts_with(strtolower($manifestPath), strtolower($stagingReal.DIRECTORY_SEPARATOR)) || ! is_file($manifestPath)) {
+ $this->error('Manifest is missing or outside the private document-import staging directory.');
+
+ return self::FAILURE;
+ }
+
+ foreach ([
+ 'documents' => ['id', 'source_system', 'external_id', 'version_no', 'data_classification', 'file_name', 'media_type', 'storage_key', 'sha256', 'status', 'size_bytes', 'issuer', 'issued_on', 'uploaded_by', 'rights_status', 'original_source', 'malware_scan_status'],
+ 'portal_resources' => ['document_id', 'title', 'category', 'topic', 'published', 'source_reference', 'approved_by', 'description', 'document_type', 'document_number', 'document_date', 'source_url', 'language', 'version_label', 'source_confirmation', 'legal_status', 'rights_status', 'document_identity_key', 'metadata_version', 'created_by'],
+ ] as $table => $columns) {
+ if (! Schema::hasColumns($table, $columns)) {
+ $this->error('Document-library schema is incomplete; run the documented schema compatibility check before importing.');
+
+ return self::FAILURE;
+ }
+ }
+
+ try {
+ $manifest = json_decode((string) file_get_contents($manifestPath), true, 512, JSON_THROW_ON_ERROR);
+ } catch (Throwable) {
+ $this->error('Manifest is not valid JSON.');
+
+ return self::FAILURE;
+ }
+ if (! is_array($manifest) || ! isset($manifest['documents']) || ! is_array($manifest['documents']) || count($manifest['documents']) > 100) {
+ $this->error('Manifest must contain at most 100 documents in a documents array.');
+
+ return self::FAILURE;
+ }
+
+ $apply = (bool) $this->option('apply');
+ $actorId = $this->option('actor');
+ if ($apply && (! is_numeric($actorId) || ! DB::table('users')->where('id', (int) $actorId)->exists())) {
+ $this->error('Apply requires --actor=; no author identity will be fabricated.');
+
+ return self::FAILURE;
+ }
+ $disk = Storage::disk('local');
+ $seenHashes = [];
+ $seenExternalIds = [];
+ $dispatch = [];
+ $failures = 0;
+
+ foreach ($manifest['documents'] as $index => $record) {
+ $label = is_array($record) ? (string) ($record['external_id'] ?? '#'.($index + 1)) : '#'.($index + 1);
+ try {
+ $validated = $this->validateRecord($record, $stagingReal);
+ $hash = $validated['sha256'];
+ if (isset($seenHashes[$hash])) {
+ $this->line($label.' | duplicate bytes within batch; references item '.$seenHashes[$hash]);
+
+ continue;
+ }
+ $seenHashes[$hash] = $label;
+ if (isset($seenExternalIds[$validated['external_id']])) {
+ $this->warn($label.' | external_id already occurs in this batch with different bytes; identity review required');
+
+ continue;
+ }
+ $seenExternalIds[$validated['external_id']] = $hash;
+
+ $existing = DB::table('documents')->where('sha256', $hash)->whereNull('deleted_at')->first();
+ if ($existing) {
+ $this->line($label.' | already imported by SHA-256; document='.$existing->id.'; no duplicate');
+
+ continue;
+ }
+ $externalMatch = DB::table('documents')->where('source_system', 'document-batch-import')
+ ->where('external_id', $validated['external_id'])->whereNull('deleted_at')->first();
+ if ($externalMatch) {
+ $this->warn($label.' | external_id already belongs to document '.$externalMatch->id.' with different bytes; version review required');
+
+ continue;
+ }
+
+ $identity = $this->identity($validated);
+ $identityMatch = DB::table('portal_resources')->where('document_identity_key', $identity)->first();
+ if ($identityMatch) {
+ $this->warn($label.' | identity matches resource '.$identityMatch->id.' but bytes differ; version review required');
+
+ continue;
+ }
+
+ $this->line($label.' | '.($apply ? 'will import as private draft' : 'preview: private draft')." | bytes={$validated['size_bytes']} | sha256={$hash}");
+ if (! $apply) {
+ continue;
+ }
+
+ $documentId = (string) Str::uuid();
+ $storageKey = 'portal-documents/'.$documentId;
+ $source = fopen($validated['source_path'], 'rb');
+ if (! is_resource($source)) {
+ throw new RuntimeException('Source could not be opened.');
+ }
+ try {
+ $stored = $disk->put($storageKey, $source);
+ } finally {
+ fclose($source);
+ }
+ $storedPath = $disk->path($storageKey);
+ $storedHash = $stored && is_file($storedPath) ? hash_file('sha256', $storedPath) : false;
+ if (! $stored || ! is_string($storedHash) || ! hash_equals($hash, $storedHash)) {
+ $disk->delete($storageKey);
+ throw new RuntimeException('Private copy failed the checksum verification.');
+ }
+
+ try {
+ $resourceId = DB::transaction(function () use ($validated, $documentId, $storageKey, $hash, $identity, $disk, $actorId): int {
+ DB::select('SELECT pg_advisory_xact_lock(hashtext(?))', ['document-batch:'.$hash]);
+ DB::select('SELECT pg_advisory_xact_lock(hashtext(?))', ['document-batch-identity:'.$identity]);
+ if (DB::table('documents')->where('sha256', $hash)->whereNull('deleted_at')->exists()) {
+ throw new RuntimeException('Matching bytes were registered concurrently; rerun preview.');
+ }
+ if (DB::table('portal_resources')->where('document_identity_key', $identity)->exists()) {
+ throw new RuntimeException('A matching document identity was registered concurrently; version review required.');
+ }
+ $now = now();
+ DB::table('documents')->insert([
+ 'id' => $documentId, 'source_system' => 'document-batch-import', 'external_id' => $validated['external_id'],
+ 'version_no' => 1, 'data_classification' => 'internal', 'file_name' => $validated['file_name'],
+ 'media_type' => $validated['media_type'], 'storage_key' => $storageKey, 'sha256' => $hash,
+ 'status' => 'quarantined', 'size_bytes' => $validated['size_bytes'], 'issuer' => $validated['issuer'],
+ 'issued_on' => $validated['document_date'], 'uploaded_by' => (int) $actorId, 'rights_status' => 'unconfirmed',
+ 'original_source' => $validated['source_url'], 'malware_scan_status' => 'pending',
+ 'created_at' => $now, 'updated_at' => $now,
+ ]);
+ $resourceId = DB::table('portal_resources')->insertGetId([
+ 'document_id' => $documentId, 'title' => $validated['title'], 'category' => $validated['category'],
+ 'topic' => $validated['topic'], 'legislation' => $validated['legislation'], 'essential' => false,
+ 'published' => false, 'source_reference' => $validated['source_reference'], 'approved_by' => null,
+ 'description' => $validated['description'], 'document_type' => $validated['document_type'],
+ 'document_number' => $validated['document_number'], 'document_date' => $validated['document_date'],
+ 'source_url' => $validated['source_url'], 'language' => $validated['language'],
+ 'version_label' => $validated['version_label'], 'territorial_scope' => 'unknown',
+ 'source_confirmation' => 'unconfirmed', 'legal_status' => 'unknown', 'rights_status' => 'unconfirmed',
+ 'document_identity_key' => $identity, 'metadata_version' => 1, 'created_by' => (int) $actorId,
+ 'last_reviewed_by' => null, 'independent_review' => false, 'created_at' => $now, 'updated_at' => $now,
+ ]);
+ $resource = DB::table('portal_resources')->where('id', $resourceId)->first();
+ DB::table('portal_resource_revisions')->insert([
+ 'resource_id' => $resourceId, 'revision_no' => 1, 'change_type' => 'created',
+ 'snapshot' => json_encode(['resource' => $resource, 'associations' => []], JSON_THROW_ON_ERROR),
+ 'actor_user_id' => (int) $actorId, 'created_at' => $now,
+ ]);
+ DB::table('audit_events')->insert([
+ 'id' => (string) Str::uuid(), 'source_system' => 'document-batch-import',
+ 'external_id' => $validated['external_id'], 'data_classification' => 'internal',
+ 'actor_user_id' => (int) $actorId, 'entity_type' => 'document', 'entity_id' => $documentId,
+ 'action' => 'private_document_batch_imported',
+ 'after_data' => json_encode(['sha256' => $hash, 'resource_id' => $resourceId, 'published' => false], JSON_THROW_ON_ERROR),
+ 'occurred_at' => $now, 'created_at' => $now,
+ ]);
+
+ return $resourceId;
+ });
+ $dispatch[] = $documentId;
+ $this->info($label.' | private draft created | resource='.$resourceId.'; pending scan; not published');
+ } catch (Throwable $exception) {
+ $disk->delete($storageKey);
+ throw $exception;
+ }
+ } catch (Throwable $exception) {
+ $this->error($label.' | not imported: '.$exception->getMessage());
+ $failures++;
+ }
+ }
+
+ foreach ($dispatch as $documentId) {
+ ScanDocumentForMalware::dispatch($documentId)->afterCommit();
+ }
+ $this->line($apply ? 'New scans queued: '.count($dispatch).'.' : 'Preview only; database and storage unchanged.');
+
+ return $failures > 0 ? self::FAILURE : self::SUCCESS;
+ }
+
+ /** @return array */
+ private function validateRecord(mixed $record, string $stagingRoot): array
+ {
+ if (! is_array($record)) {
+ throw new RuntimeException('Each documents entry must be an object.');
+ }
+ foreach (['external_id', 'relative_path', 'title', 'issuer'] as $required) {
+ if (! isset($record[$required]) || ! is_string($record[$required]) || trim($record[$required]) === '') {
+ throw new RuntimeException('Required manifest field is missing: '.$required.'.');
+ }
+ }
+ if (strlen($record['external_id']) > 120 || strlen($record['title']) > 250 || strlen($record['issuer']) > 180) {
+ throw new RuntimeException('A metadata value exceeds its permitted length.');
+ }
+ if (isset($record['file_name']) && (! is_string($record['file_name']) || strlen(basename($record['file_name'])) > 255)) {
+ throw new RuntimeException('File name exceeds its permitted length.');
+ }
+ if (isset($record['document_number']) && (! is_string($record['document_number']) || strlen($record['document_number']) > 120)) {
+ throw new RuntimeException('Document number exceeds its permitted length.');
+ }
+ if (isset($record['document_date'])) {
+ if (! is_string($record['document_date']) || ! preg_match('/^(\d{4})-(\d{2})-(\d{2})$/', $record['document_date'], $dateParts)
+ || ! checkdate((int) $dateParts[2], (int) $dateParts[3], (int) $dateParts[1])) {
+ throw new RuntimeException('Document date must use a valid YYYY-MM-DD value.');
+ }
+ }
+ if (isset($record['document_type']) && ! in_array($record['document_type'], ['legislation', 'procedure', 'official_form', 'digital_adaptation', 'guidance_template', 'guide', 'study', 'reference_data', 'unknown'], true)) {
+ throw new RuntimeException('Document type is not supported by the catalogue.');
+ }
+
+ $path = realpath($stagingRoot.DIRECTORY_SEPARATOR.$record['relative_path']);
+ if (! $path || ! str_starts_with(strtolower($path), strtolower($stagingRoot.DIRECTORY_SEPARATOR)) || ! is_file($path)) {
+ throw new RuntimeException('Source file is missing or outside the private staging directory.');
+ }
+ $size = filesize($path);
+ if (! is_int($size) || $size < 1 || $size > self::MAX_BYTES) {
+ throw new RuntimeException('File size is outside the 1 byte to 10 MiB import limit.');
+ }
+ $mime = (new \finfo(FILEINFO_MIME_TYPE))->file($path);
+ $allowed = ['application/pdf', 'text/plain', 'text/csv', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
+ 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', 'image/png', 'image/jpeg', 'image/webp'];
+ if (! is_string($mime) || ! in_array($mime, $allowed, true)) {
+ throw new RuntimeException('Detected media type is not supported by the document library.');
+ }
+ $sha = hash_file('sha256', $path);
+ if (! is_string($sha)) {
+ throw new RuntimeException('File hash could not be calculated.');
+ }
+
+ $sourceUrl = $record['source_url'] ?? null;
+ if ($sourceUrl !== null && (! is_string($sourceUrl) || strlen($sourceUrl) > 2000 || ! filter_var($sourceUrl, FILTER_VALIDATE_URL) || strtolower((string) parse_url($sourceUrl, PHP_URL_SCHEME)) !== 'https')) {
+ throw new RuntimeException('Source URL, when supplied, must be a valid HTTPS URL.');
+ }
+
+ return [
+ ...$record, 'source_path' => $path, 'file_name' => basename((string) ($record['file_name'] ?? $path)),
+ 'size_bytes' => $size, 'media_type' => $mime, 'sha256' => $sha,
+ 'category' => (string) ($record['category'] ?? 'general'), 'topic' => (string) ($record['topic'] ?? 'unclassified'),
+ 'legislation' => (bool) ($record['legislation'] ?? false), 'document_type' => (string) ($record['document_type'] ?? 'unknown'),
+ 'document_number' => $record['document_number'] ?? null, 'document_date' => $record['document_date'] ?? null,
+ 'language' => (string) ($record['language'] ?? 'unknown'), 'version_label' => $record['version_label'] ?? null,
+ 'source_url' => $record['source_url'] ?? null, 'source_reference' => $record['source_reference'] ?? 'Origem por completar.',
+ 'description' => $record['description'] ?? 'Documento importado para revisão documental.',
+ ];
+ }
+
+ /** @param array $record */
+ private function identity(array $record): string
+ {
+ return hash('sha256', mb_strtolower(implode('|', array_map('trim', [
+ (string) $record['issuer'], (string) ($record['document_number'] ?? ''), (string) ($record['document_date'] ?? ''),
+ (string) $record['language'], (string) ($record['version_label'] ?? $record['title']),
+ ]))));
+ }
+}
diff --git a/app/Console/Commands/ImportLegalSourceArchive.php b/app/Console/Commands/ImportLegalSourceArchive.php
index 0012336d..3674c665 100644
--- a/app/Console/Commands/ImportLegalSourceArchive.php
+++ b/app/Console/Commands/ImportLegalSourceArchive.php
@@ -102,7 +102,7 @@ class ImportLegalSourceArchive extends Command
$links = DB::table('portal_resource_links')->where('resource_id', $resource->id)->orderBy('id')->get();
$snapshot = DB::table('portal_resources')->where('id', $resource->id)->first();
DB::table('portal_resource_revisions')->insert(['resource_id' => $resource->id, 'revision_no' => $version,
- 'change_type' => 'requirement_evidence_associated',
+ 'change_type' => 'evidence_linked',
'snapshot' => json_encode(['resource' => $snapshot, 'associations' => $links], JSON_THROW_ON_ERROR),
'actor_user_id' => null, 'created_at' => $now]);
}
diff --git a/app/Http/Controllers/InvestmentZoneController.php b/app/Http/Controllers/InvestmentZoneController.php
index 8ffdbe43..48b4e02f 100644
--- a/app/Http/Controllers/InvestmentZoneController.php
+++ b/app/Http/Controllers/InvestmentZoneController.php
@@ -32,7 +32,7 @@ class InvestmentZoneController extends Controller
$locations = DB::table('investment_zone_locations as zl')->join('geo_locations as g', 'g.id', '=', 'zl.geo_location_id')
->where('zl.zone_id', $id)->when(! $management, fn ($q) => $q->where('zl.validation_status', 'validated')->where('g.data_classification', 'public'))
->whereNull('g.deleted_at')->orderBy('g.location_type')->orderBy('g.name')->get(['g.id', 'g.name', 'g.location_type', 'g.parent_id', 'zl.scope_type', 'zl.source_reference', 'zl.validation_status']);
- $source = DB::table('investment_legal_sources')->where('id', $zone->legal_source_id)->first(['title', 'url', 'diploma', 'article', 'competent_entity', 'valid_from', 'valid_until', 'version_no']);
+ $source = DB::table('investment_legal_sources')->where('id', $zone->legal_source_id)->first(['title', 'url', 'diploma', 'article', 'competent_entity', 'valid_from', 'valid_until', 'validation_status', 'version_no']);
$operator = DB::table('organizations')->where('id', $zone->operator_id)->whereNull('deleted_at')
->when(! $management, fn ($q) => $q->where('data_classification', 'public'))->value('name');
$field = ['zee' => 'zee_id', 'zfi' => 'zfi_id', 'industrial_park' => 'park_id'][$zone->category] ?? null;
@@ -125,7 +125,7 @@ class InvestmentZoneController extends Controller
$stream = fopen('php://output', 'wb');
fwrite($stream, "\xEF\xBB\xBF");
fputcsv($stream, ['Código', 'Nome', 'Categoria física', 'Regime', 'Desenvolvimento', 'Disponibilidade', 'Província', 'Distrito', 'Área declarada', 'Unidade', 'Fontes'], ';');
- $query->chunk(200, function ($zones) use ($stream): void {
+ $query->chunk(200, function ($zones) use ($stream, $geography): void {
foreach ($zones as $zone) {
$territoryPath = $geography->path($zone->primary_territory_id);
$province = collect($territoryPath)->firstWhere('location_type', 'province')->name ?? $zone->province;
@@ -221,9 +221,6 @@ class InvestmentZoneController extends Controller
public function transition(Request $request, InvestmentZones $zones, string $id, string $action): JsonResponse
{
- if ($action === 'publish') {
- InvestmentZoneAccess::requireRole($request->user(), 'publishers');
- }
$data = $request->validate(['version_no' => ['required', 'integer', 'min:1']]);
return response()->json($action === 'publish' ? $zones->publish($id, $data['version_no'], $request->user()->id)
diff --git a/app/Http/Controllers/PortalServicesController.php b/app/Http/Controllers/PortalServicesController.php
index 697a7fca..44d737a3 100644
--- a/app/Http/Controllers/PortalServicesController.php
+++ b/app/Http/Controllers/PortalServicesController.php
@@ -13,6 +13,7 @@ use Illuminate\Database\QueryException;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
+use Illuminate\Support\Facades\Schema;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;
use Illuminate\Validation\ValidationException;
@@ -108,7 +109,11 @@ class PortalServicesController extends Controller
])->paginate(20)->withQueryString();
$pageResult->getCollection()->transform(function ($resource) {
$resource->links = DB::table('portal_resource_links')->where('resource_id', $resource->id)
- ->orderBy('target_type')->orderBy('target_key')->get(['target_type', 'target_key', 'relationship', 'applicability', 'condition_text', 'basis_reference', 'review_status']);
+ ->orderBy('target_type')->orderBy('target_key')->get($this->resourceLinkColumns());
+ $resource->links->transform(fn ($link) => (object) [...(array) $link,
+ 'target_label' => $link->target_type === 'requirement'
+ ? config('document-requirements.catalogue.'.$link->target_key.'.label', $link->target_key)
+ : null]);
$resource->download_url = route('services.download', ['id' => $resource->id]);
$resource->view_url = in_array($resource->media_type, ['application/pdf', 'image/jpeg', 'image/png', 'image/webp'], true)
? route('services.document.view', ['id' => $resource->id]) : null;
@@ -186,8 +191,10 @@ class PortalServicesController extends Controller
$resources->transform(function ($resource) use ($matches): array {
$matched = DB::table('portal_resource_links')->where('resource_id', $resource->id)->whereIn('target_type', array_column($matches, 'type'))
->whereIn('target_key', array_column($matches, 'key'))->orderBy('target_type')->first(['target_type', 'target_key', 'applicability', 'condition_text', 'basis_reference']);
- $evidence = DB::table('portal_resource_links')->where('resource_id', $resource->id)->where('target_type', 'requirement')
- ->where('review_status', 'confirmed')->orderBy('target_key')->get(['target_key', 'basis_reference', 'review_status']);
+ $evidence = $this->requirementEvidenceEnabled()
+ ? DB::table('portal_resource_links')->where('resource_id', $resource->id)->where('target_type', 'requirement')
+ ->where('review_status', 'confirmed')->orderBy('target_key')->get(['target_key', 'basis_reference', 'review_status'])
+ : collect();
return [...(array) $resource,
'download_url' => route('services.download', ['id' => $resource->id]),
'recommendation_reason' => $matched?->condition_text ?? ('Relacionado com '.$matched?->target_type.' '.$matched?->target_key.'; confirme a aplicabilidade ao seu projecto.'),
@@ -268,7 +275,11 @@ class PortalServicesController extends Controller
abort_unless($resource !== null, 404);
$resource->links = DB::table('portal_resource_links')->where('resource_id', $id)
->orderBy('target_type')->orderBy('target_key')
- ->get(['target_type', 'target_key', 'relationship', 'applicability', 'condition_text', 'basis_reference', 'review_status']);
+ ->get($this->resourceLinkColumns());
+ $resource->links->transform(fn ($link) => (object) [...(array) $link,
+ 'target_label' => $link->target_type === 'requirement'
+ ? config('document-requirements.catalogue.'.$link->target_key.'.label', $link->target_key)
+ : null]);
$territory = $resource->territory_id
? DB::table('geo_locations')->where('id', $resource->territory_id)->value('name')
: null;
@@ -326,6 +337,10 @@ class PortalServicesController extends Controller
public function manage(MalwareScanner $scanner): Response
{
+ $disclosureAuthorization = DB::table('audit_events')->where('source_system', 'user-instruction')
+ ->where('external_id', 'document-repository-disclosure-authorization-2026-10-06-v1')
+ ->where('action', 'public_disclosure_authorized')->first(['external_id', 'occurred_at', 'after_data']);
+ $disclosureAuthorizationData = $disclosureAuthorization ? json_decode($disclosureAuthorization->after_data, true) : null;
$resources = DB::table('portal_resources as r')->join('documents as d', 'd.id', '=', 'r.document_id')->orderByDesc('r.updated_at')->limit(200)
->get(['r.*', 'd.file_name', 'd.media_type', 'd.size_bytes', 'd.issuer', 'd.status as file_status', 'd.data_classification', 'd.version_no as file_version', 'd.rights_status as file_rights_status', 'd.malware_scan_status', 'd.malware_failure_reason']);
$links = DB::table('portal_resource_links')->whereIn('resource_id', $resources->pluck('id'))->orderBy('id')->get()->groupBy('resource_id');
@@ -337,11 +352,20 @@ class PortalServicesController extends Controller
$malwareHealth['signatures_at'] = $malwareHealth['signatures_at']?->format(DATE_ATOM);
return Inertia::render('portal/manage', ['resources' => $resources,
- 'requirementCatalogue' => collect(config('document-requirements.catalogue'))->map(fn ($item, $key) => ['key' => $key, ...$item])->values(),
+ 'requirementCatalogue' => $this->requirementEvidenceEnabled()
+ ? collect(config('document-requirements.catalogue'))->map(fn ($item, $key) => ['key' => $key, ...$item])->values()
+ : collect(),
+ 'requirementAssociationsEnabled' => $this->requirementEvidenceEnabled(),
'partnerships' => DB::table('portal_partnerships')->latest()->limit(100)->get(),
'documents' => DB::table('documents')->where('status', '!=', 'deleted')->whereNull('deleted_at')->latest()->limit(250)
->get(['id', 'file_name', 'media_type', 'size_bytes', 'issuer', 'issued_on', 'valid_until', 'uploaded_by', 'created_at', 'status', 'data_classification', 'version_no', 'sha256', 'rights_status', 'malware_scan_status', 'malware_scanned_at', 'malware_failure_reason', 'malware_engine_version', 'malware_signatures_at', 'malware_scan_attempts']),
'malwareHealth' => $malwareHealth,
+ 'disclosureAuthorization' => $disclosureAuthorizationData ? [
+ 'external_id' => $disclosureAuthorization->external_id,
+ 'occurred_at' => $disclosureAuthorization->occurred_at,
+ 'resources' => DB::table('portal_resources')->whereIn('id', $disclosureAuthorizationData['resource_ids'] ?? [])
+ ->orderBy('id')->get(['id', 'title', 'published', 'rights_status']),
+ ] : null,
'malwareCounts' => DB::table('documents')->whereNull('deleted_at')->where('status', '!=', 'deleted')
->select('malware_scan_status', DB::raw('count(*) as total'))->groupBy('malware_scan_status')->pluck('total', 'malware_scan_status'),
'territories' => DB::table('geo_locations')->where('data_classification', 'public')->whereNull('deleted_at')
@@ -355,12 +379,12 @@ class PortalServicesController extends Controller
public function upload(Request $request): RedirectResponse
{
- $request->validate(['file' => ['required', 'file', 'mimetypes:application/pdf,text/plain,text/csv,image/jpeg,image/png,image/webp,application/msword,application/vnd.openxmlformats-officedocument.wordprocessingml.document,application/vnd.openxmlformats-officedocument.spreadsheetml.sheet,application/vnd.oasis.opendocument.text', 'max:10240'],
+ $this->validateManagement($request, ['file' => ['required', 'file', 'mimetypes:application/pdf,text/plain,text/csv,image/jpeg,image/png,image/webp,application/msword,application/vnd.openxmlformats-officedocument.wordprocessingml.document,application/vnd.openxmlformats-officedocument.spreadsheetml.sheet,application/vnd.oasis.opendocument.text', 'max:10240'],
'original_source' => ['nullable', 'url:https', 'max:2000'], 'issuer' => ['nullable', 'string', 'max:180']]);
$file = $request->file('file');
$checksum = hash_file('sha256', $file->getRealPath());
if (DB::table('documents')->where('sha256', $checksum)->whereNull('deleted_at')->exists()) {
- return back()->withErrors(['file' => 'Já existe um ficheiro com o mesmo conteúdo. Verifique se corresponde a uma nova versão ou a um duplicado.']);
+ return to_route('services.manage')->withErrors(['file' => 'Já existe um ficheiro com o mesmo conteúdo. Verifique se corresponde a uma nova versão ou a um duplicado.']);
}
$id = (string) Str::uuid();
$key = $file->storeAs('portal-documents', $id, 'local');
@@ -385,7 +409,7 @@ class PortalServicesController extends Controller
ScanDocumentForMalware::dispatch($id)->afterCommit();
- return back()->with('success', 'Documento recebido em armazenamento privado e quarentena. Não aparece na biblioteca pública até validação e publicação explícitas.');
+ return to_route('services.manage')->with('success', 'Documento recebido em armazenamento privado e quarentena. Não aparece na biblioteca pública até validação e publicação explícitas.');
}
public function resource(Request $request): RedirectResponse
@@ -412,7 +436,7 @@ class PortalServicesController extends Controller
return $id;
});
- return back()->with('success', 'Documento registado como rascunho privado da biblioteca. A publicação requer uma acção separada.');
+ return to_route('services.manage')->with('success', 'Documento registado como rascunho privado da biblioteca. A publicação requer uma acção separada.');
}
public function updateResource(Request $request, int $id): RedirectResponse
@@ -448,12 +472,12 @@ class PortalServicesController extends Controller
$this->audit($request, 'resource_metadata_updated', 'portal_resources', $id);
});
- return back()->with('success', 'Metadados e relações actualizados; o estado de publicação foi preservado.');
+ return to_route('services.manage')->with('success', 'Metadados e relações actualizados; o estado de publicação foi preservado.');
}
public function moderate(Request $request, string $type, int $id): RedirectResponse
{
- $data = $request->validate(['published' => ['required', 'boolean']]);
+ $data = $this->validateManagement($request, ['published' => ['required', 'boolean']]);
$table = $type === 'resources' ? 'portal_resources' : 'portal_partnerships';
DB::transaction(function () use ($request, $data, $table, $type, $id) {
abort_unless(DB::table($table)->where('id', $id)->lockForUpdate()->first() !== null, 404);
@@ -462,18 +486,22 @@ class PortalServicesController extends Controller
->where('r.id', $id)->first(['r.document_type', 'r.legislation', 'r.source_confirmation', 'r.legal_status', 'r.rights_status',
'r.source_url', 'd.status', 'd.data_classification', 'd.deleted_at', 'd.rights_status as file_rights_status',
'd.malware_scan_status', 'd.malware_scanned_sha256', 'd.sha256']);
- abort_unless($resource && $resource->status === 'active' && $resource->data_classification === 'public' && $resource->deleted_at === null,
- 422, 'O ficheiro continua privado/em quarentena e não pode ser publicado.');
- abort_unless($resource->rights_status === 'authorized' && $resource->file_rights_status === 'authorized',
- 422, 'Confirme e registe os direitos de utilização antes de publicar.');
- abort_unless($resource->source_confirmation !== 'unconfirmed' && $resource->source_url,
- 422, 'Identifique e confirme a fonte documental antes de publicar.');
- abort_unless($resource->malware_scan_status === 'clean'
- && hash_equals(trim($resource->sha256), trim((string) $resource->malware_scanned_sha256)), 422,
- 'A análise antimalware tem de terminar antes da publicação.');
- if ($resource->document_type === 'legislation' || (bool) $resource->legislation) {
- abort_unless($resource->source_confirmation === 'official_confirmed',
- 422, 'Diplomas exigem confirmação da fonte oficial. Se a vigência não estiver esclarecida, publique-a como por confirmar.');
+ abort_unless($resource !== null && $resource->deleted_at === null, 404);
+ if ($resource->status !== 'active') {
+ throw $this->managementValidationException(['published' => 'A análise técnica ainda não libertou o ficheiro da quarentena. Aguarde a análise antimalware antes de publicar.']);
+ }
+ if ($resource->data_classification !== 'public') {
+ throw $this->managementValidationException(['published' => 'O ficheiro já saiu da quarentena técnica, mas continua classificado como interno. Reveja os direitos e a possibilidade de divulgação; só então altere explicitamente a classificação para pública.']);
+ }
+ if ($resource->rights_status !== 'authorized' || $resource->file_rights_status !== 'authorized') {
+ throw $this->managementValidationException(['published' => 'Confirme e registe os direitos de utilização antes de publicar.']);
+ }
+ if ($resource->source_confirmation === 'unconfirmed' || ! $resource->source_url) {
+ throw $this->managementValidationException(['published' => 'Identifique e confirme a fonte documental antes de publicar.']);
+ }
+ if ($resource->malware_scan_status !== 'clean'
+ || ! hash_equals(trim($resource->sha256), trim((string) $resource->malware_scanned_sha256))) {
+ throw $this->managementValidationException(['published' => 'A análise antimalware tem de terminar antes da publicação.']);
}
}
DB::table($table)->where('id', $id)->update($type === 'resources'
@@ -487,18 +515,128 @@ class PortalServicesController extends Controller
$this->audit($request, $data['published'] ? 'published' : 'withdrawn', $table, $id);
});
- return back()->with('success', 'Publicação actualizada.');
+ return to_route('services.manage')->with('success', 'Publicação actualizada.');
}
- private function audit(Request $request, string $action, string $table, int $id): void
+ public function applyDocumentDisclosureAuthorization(Request $request): RedirectResponse
+ {
+ $event = DB::table('audit_events')->where('source_system', 'user-instruction')
+ ->where('external_id', 'document-repository-disclosure-authorization-2026-10-06-v1')
+ ->where('action', 'public_disclosure_authorized')->first();
+ abort_unless($event !== null, 404);
+ $authorization = json_decode($event->after_data, true);
+ $resourceIds = array_values(array_unique(array_map('intval', $authorization['resource_ids'] ?? [])));
+ abort_unless(count($resourceIds) === 4, 409, 'O âmbito da autorização registada foi alterado; interrompida a publicação.');
+
+ $published = [];
+ $skipped = [];
+ DB::transaction(function () use ($request, $event, $resourceIds, &$published, &$skipped): void {
+ foreach ($resourceIds as $resourceId) {
+ $resource = DB::table('portal_resources as r')->join('documents as d', 'd.id', '=', 'r.document_id')
+ ->where('r.id', $resourceId)->lockForUpdate()->first([
+ 'r.id', 'r.title', 'r.document_type', 'r.legislation', 'r.source_url', 'r.source_confirmation',
+ 'r.rights_status as resource_rights', 'r.published', 'r.approved_by', 'r.metadata_version',
+ 'd.id as document_id', 'd.storage_key', 'd.sha256', 'd.status', 'd.deleted_at', 'd.rights_status as file_rights',
+ 'd.malware_scan_status', 'd.malware_scanned_sha256', 'd.data_classification',
+ ]);
+ if ($resource === null) {
+ $skipped[$resourceId] = 'Registo ou ficheiro inexistente.';
+ continue;
+ }
+ if ($resource->published && $resource->data_classification === 'public' && $resource->resource_rights === 'authorized' && $resource->file_rights === 'authorized') {
+ $published[$resourceId] = 'Já publicado.';
+ continue;
+ }
+ $path = Storage::disk('local')->path($resource->storage_key);
+ $root = realpath(Storage::disk('local')->path(''));
+ $realPath = realpath($path);
+ $actualHash = $realPath && is_file($realPath) && $root
+ && str_starts_with(strtolower($realPath), strtolower($root.DIRECTORY_SEPARATOR))
+ ? hash_file('sha256', $realPath) : false;
+ if ($resource->deleted_at !== null || $resource->status !== 'active'
+ || $resource->malware_scan_status !== 'clean' || ! is_string($actualHash)
+ || ! hash_equals((string) $resource->sha256, $actualHash)
+ || ! hash_equals((string) $resource->sha256, (string) $resource->malware_scanned_sha256)) {
+ $skipped[$resourceId] = 'Ficheiro indisponível, não limpo ou diferente dos bytes analisados.';
+ continue;
+ }
+ if (in_array($resource->resource_rights, ['restricted'], true) || in_array($resource->file_rights, ['restricted'], true)
+ || $resource->source_confirmation === 'unconfirmed' || ! filter_var($resource->source_url, FILTER_VALIDATE_URL)
+ || parse_url($resource->source_url, PHP_URL_SCHEME) !== 'https') {
+ $skipped[$resourceId] = 'Restrição de divulgação ou origem identificada em falta.';
+ continue;
+ }
+
+ $now = now();
+ $nextVersion = (int) $resource->metadata_version + 1;
+ DB::table('documents')->where('id', $resource->document_id)->update([
+ 'rights_status' => 'authorized', 'data_classification' => 'public', 'updated_at' => $now,
+ ]);
+ DB::table('portal_resources')->where('id', $resourceId)->update([
+ 'rights_status' => 'authorized', 'published' => true, 'approved_by' => $request->user()->id,
+ 'last_reviewed_by' => $request->user()->id, 'last_reviewed_at' => $now,
+ 'metadata_version' => $nextVersion, 'updated_at' => $now,
+ ]);
+ $this->attachAuthorizedDocumentaryEvidence($resourceId, $resource->title);
+ $this->recordResourceRevision($resourceId, 'published', $request->user()->id, $nextVersion);
+ $this->audit($request, 'published_under_user_authorization', 'portal_resources', $resourceId, [
+ 'authorization_source_system' => $event->source_system,
+ 'authorization_external_id' => $event->external_id,
+ 'source_confirmation' => $resource->source_confirmation,
+ 'legal_status_preserved' => true,
+ ]);
+ $published[$resourceId] = 'Publicado com a situação de fonte e vigência já registada.';
+ }
+ });
+
+ return to_route('services.manage')->with('success', count($published).' documento(s) disponibilizado(s); '.count($skipped).' pendência(s) individual(is).')
+ ->with('disclosureResults', ['published' => $published, 'skipped' => $skipped]);
+ }
+
+ private function audit(Request $request, string $action, string $table, int $id, array $additional = []): void
{
DB::table('audit_events')->insert(['source_system' => 'portal-services', 'entity_type' => $table,
- 'actor_user_id' => $request->user()->id, 'action' => $action, 'after_data' => json_encode(['record_id' => $id], JSON_THROW_ON_ERROR)]);
+ 'actor_user_id' => $request->user()->id, 'action' => $action,
+ 'after_data' => json_encode(['record_id' => $id, ...$additional], JSON_THROW_ON_ERROR)]);
+ }
+
+ private function attachAuthorizedDocumentaryEvidence(int $resourceId, string $title): void
+ {
+ if (! $this->requirementEvidenceEnabled()) {
+ return;
+ }
+
+ $evidence = match (true) {
+ str_contains($title, 'Lei n.º 22/2022') => [['fiscal.iva.lei_22_2022', 'confirmed', 'Lei n.º 22/2022, art. 17(1); citação conferida no exemplar cujo SHA-256 coincide com o ficheiro oficial. Isto é evidência documental, não valida vigência ou cálculo.']],
+ str_contains($title, 'BR 248/2025') || str_contains($title, 'n.º 248/2025') => array_map(fn (array $law) => ['fiscal.'.$law[0].'.'.$law[1], 'pending', 'BR n.º 248/2025, Suplemento n.º 1, '.$law[2].'; artigo/dispositivo específico não conferido nesta associação. Evidência identificada, não fundamento confirmado.'], [
+ ['ice', 'lei_7_2025', 'Lei n.º 7/2025'], ['customs', 'lei_8_2025', 'Lei n.º 8/2025'],
+ ['ispc', 'lei_9_2025', 'Lei n.º 9/2025'], ['iva', 'lei_10_2025', 'Lei n.º 10/2025'],
+ ['irps', 'lei_11_2025', 'Lei n.º 11/2025'], ['irpc', 'lei_12_2025', 'Lei n.º 12/2025'],
+ ]),
+ str_contains($title, 'Lei n.º 13/2016') || str_contains($title, 'BR n.º 89') => [['fiscal.iva.lei_13_2016_rectification_2017', 'pending', 'BR n.º 89/2017, I Série, p. 569, aviso relativo à Lei n.º 13/2016; a correspondência do exemplar local e os dispositivos rectificados aguardam conferência. Evidência identificada, não fundamento confirmado.']],
+ default => [],
+ };
+
+ foreach ($evidence as [$key, $reviewStatus, $basis]) {
+ if (! array_key_exists($key, config('document-requirements.catalogue', []))) {
+ continue;
+ }
+ $exists = DB::table('portal_resource_links')->where('resource_id', $resourceId)
+ ->where('target_type', 'requirement')->where('target_key', $key)->exists();
+ if ($exists) {
+ continue;
+ }
+ DB::table('portal_resource_links')->insert([
+ 'resource_id' => $resourceId, 'target_type' => 'requirement', 'target_key' => $key,
+ 'relationship' => 'cites', 'applicability' => 'evidence', 'basis_reference' => $basis,
+ 'review_status' => $reviewStatus, 'created_at' => now(), 'updated_at' => now(),
+ ]);
+ }
}
private function validateResource(Request $request): array
{
- $data = $request->validate([
+ $data = $this->validateManagement($request, [
'document_id' => ['required', 'uuid'], 'title' => ['required', 'string', 'max:200'],
'description' => ['nullable', 'string', 'max:4000'], 'category' => ['required', 'string', 'max:80'],
'topic' => ['required', 'string', 'max:80'], 'document_type' => ['required', 'in:legislation,procedure,official_form,digital_adaptation,guidance_template,guide,study,reference_data,unknown'],
@@ -515,7 +653,7 @@ class PortalServicesController extends Controller
'data_classification' => ['nullable', 'in:internal,public'],
'source_reference' => ['required', 'string', 'min:10', 'max:2000'],
'legislation' => ['required', 'boolean'], 'essential' => ['required', 'boolean'],
- 'associations' => ['nullable', 'array', 'max:40'], 'associations.*.target_type' => ['required', 'in:theme,module,activity,stage,procedure,entity,territory,document,investment_type,legal_source,requirement'],
+ 'associations' => ['nullable', 'array', 'max:40'], 'associations.*.target_type' => ['required', 'in:theme,module,activity,stage,procedure,entity,territory,document,investment_type,legal_source'.($this->requirementEvidenceEnabled() ? ',requirement' : '')],
'associations.*.target_key' => ['required', 'string', 'max:180'],
'associations.*.review_status' => ['nullable', 'in:pending,confirmed'],
'associations.*.relationship' => ['required', 'in:applies_to,supports,version_of,translation_of,amends,repeals,implements,cites,related_to'],
@@ -523,24 +661,47 @@ class PortalServicesController extends Controller
'associations.*.condition_text' => ['nullable', 'string', 'max:2000'], 'associations.*.basis_reference' => ['nullable', 'string', 'max:2000'],
]);
if (! empty($data['applicable_from']) && ! empty($data['applicable_until']) && $data['applicable_until'] < $data['applicable_from']) {
- throw ValidationException::withMessages(['applicable_until' => 'A data final deve ser igual ou posterior à data inicial.']);
+ throw $this->managementValidationException(['applicable_until' => 'A data final deve ser igual ou posterior à data inicial.']);
}
foreach ($data['associations'] ?? [] as $index => $association) {
+ if (($association['target_type'] ?? null) === 'requirement' && ! $this->requirementEvidenceEnabled()) {
+ throw $this->managementValidationException(["associations.$index.target_type" => 'As associações documentais a requisitos ainda aguardam a migração do catálogo.']);
+ }
if (($association['target_type'] ?? null) === 'requirement'
&& ! array_key_exists($association['target_key'], config('document-requirements.catalogue', []))) {
- throw ValidationException::withMessages(["associations.$index.target_key" => 'Seleccione um identificador técnico de requisito existente.']);
+ throw $this->managementValidationException(["associations.$index.target_key" => 'Seleccione um identificador técnico de requisito existente.']);
}
if (mb_strlen(trim($association['basis_reference'] ?? '')) < 5) {
- throw ValidationException::withMessages(["associations.$index.basis_reference" => 'Indique o artigo, secção, página ou outro trecho que esta associação sustenta.']);
+ throw $this->managementValidationException(["associations.$index.basis_reference" => 'Indique o artigo, secção, página ou outro trecho que esta associação sustenta.']);
}
if ($association['applicability'] === 'conditional' && mb_strlen(trim($association['condition_text'] ?? '')) < 5) {
- throw ValidationException::withMessages(["associations.$index.condition_text" => 'Descreva a condição de aplicação.']);
+ throw $this->managementValidationException(["associations.$index.condition_text" => 'Descreva a condição de aplicação.']);
}
}
return $data;
}
+ /** Keep validation redirects on the management GET page even if the request
+ * arrived with a stale Referer pointing at a write-only resource endpoint.
+ */
+ private function validateManagement(Request $request, array $rules): array
+ {
+ try {
+ return $request->validate($rules);
+ } catch (ValidationException $exception) {
+ throw $this->managementValidationException($exception->errors());
+ }
+ }
+
+ private function managementValidationException(array $errors): ValidationException
+ {
+ $exception = ValidationException::withMessages($errors);
+ $exception->redirectTo = route('services.manage');
+
+ return $exception;
+ }
+
private function resourceFields(array $data, int $actor, ?string $identity, ?int $duplicate, bool $creating): array
{
unset($data['associations'], $data['issuer'], $data['data_classification']);
@@ -567,7 +728,7 @@ class PortalServicesController extends Controller
'resource_id' => $resourceId, 'target_type' => $link['target_type'], 'target_key' => $link['target_key'],
'relationship' => $link['relationship'], 'applicability' => $link['applicability'],
'condition_text' => $link['condition_text'] ?? null, 'basis_reference' => $link['basis_reference'] ?? null,
- 'review_status' => $link['review_status'] ?? 'pending',
+ ...($this->requirementEvidenceEnabled() ? ['review_status' => $link['review_status'] ?? 'pending'] : []),
'created_at' => now(), 'updated_at' => now(),
]);
}
@@ -586,10 +747,6 @@ class PortalServicesController extends Controller
'Registe a autorização de utilização antes de classificar o ficheiro como público.');
abort_unless($data['source_confirmation'] !== 'unconfirmed' && ! empty($data['source_url']), 422,
'Identifique e confirme a fonte antes de classificar o ficheiro como público.');
- if (($data['legislation'] ?? false) || ($data['document_type'] ?? null) === 'legislation') {
- abort_unless($data['source_confirmation'] === 'official_confirmed', 422,
- 'Diplomas exigem confirmação da fonte oficial antes de serem classificados como públicos.');
- }
}
private function identityKey(array $data): ?string
@@ -611,6 +768,23 @@ class PortalServicesController extends Controller
'actor_user_id' => $actor, 'created_at' => now()]);
}
+ private function requirementEvidenceEnabled(): bool
+ {
+ return Schema::hasColumn('portal_resource_links', 'review_status')
+ && Schema::hasTable('migrations')
+ && DB::table('migrations')->where('migration', '2026_10_06_200000_add_document_requirement_evidence_links')->exists();
+ }
+
+ private function resourceLinkColumns(): array
+ {
+ $columns = ['target_type', 'target_key', 'relationship', 'applicability', 'condition_text', 'basis_reference'];
+ if (Schema::hasColumn('portal_resource_links', 'review_status')) {
+ $columns[] = 'review_status';
+ }
+
+ return $columns;
+ }
+
private function territoryScopeIds(string $id): array
{
return collect(DB::select(<<<'SQL'
diff --git a/app/Http/Controllers/PublicBusinessOpportunityController.php b/app/Http/Controllers/PublicBusinessOpportunityController.php
index abd857bd..5688f970 100644
--- a/app/Http/Controllers/PublicBusinessOpportunityController.php
+++ b/app/Http/Controllers/PublicBusinessOpportunityController.php
@@ -10,6 +10,7 @@ use Illuminate\Database\Eloquent\Builder;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
+use Illuminate\Validation\Rule;
use Inertia\Inertia;
use Inertia\Response;
@@ -19,6 +20,7 @@ class PublicBusinessOpportunityController extends Controller
{
$filters = $request->validate([
'q' => ['nullable', 'string', 'max:120'],
+ 'modality' => ['nullable', Rule::in(ExistingBusinessDrafts::MODALITIES)],
'sector' => ['nullable', 'integer', 'min:1'],
'per_page' => ['nullable', 'integer', 'min:1', 'max:50'],
'province_id' => ['nullable', 'uuid'], 'district_id' => ['nullable', 'uuid'],
@@ -46,6 +48,9 @@ class PublicBusinessOpportunityController extends Controller
if (! empty($filters['sector'])) {
$query->where('existing_businesses.sector_id', $filters['sector']);
}
+ if (! empty($filters['modality'])) {
+ $query->where('existing_businesses.modality', $filters['modality']);
+ }
$results = PublicBusinessOpportunityResource::collection($query->paginate($filters['per_page'] ?? 12)->withQueryString());
if ($request->expectsJson()) {
@@ -65,7 +70,7 @@ class PublicBusinessOpportunityController extends Controller
$returnFilters = $request->query();
- return Inertia::render('investment/business-opportunity', ['record' => $resource,
+ return Inertia::render('investment/business-opportunity', ['record' => $resource->resolve($request),
'returnUrl' => $returnFilters === [] ? '/negocios' : '/negocios?'.http_build_query($returnFilters),
'interestUrl' => '/negocios/'.rawurlencode($slug).'/manifestar-interesse'.($returnFilters === [] ? '' : '?'.http_build_query($returnFilters))]);
}
@@ -98,7 +103,7 @@ class PublicBusinessOpportunityController extends Controller
return ExistingBusiness::query()->published()->with('sector:id,name,slug')
->leftJoinSub($publicLocations, 'public_location', 'public_location.existing_business_id', '=', 'existing_businesses.id')
- ->select(['existing_businesses.id', 'existing_businesses.public_slug', 'existing_businesses.public_title',
+ ->select(['existing_businesses.id', 'existing_businesses.public_slug', 'existing_businesses.public_title', 'existing_businesses.modality',
'existing_businesses.activity_code', 'existing_businesses.stage', 'existing_businesses.objective',
'existing_businesses.currency', 'existing_businesses.amount_min', 'existing_businesses.amount_max',
'existing_businesses.available_percentage', 'existing_businesses.published_at', 'existing_businesses.sector_id',
diff --git a/app/Http/Resources/PublicBusinessOpportunityResource.php b/app/Http/Resources/PublicBusinessOpportunityResource.php
index 9a5acb1a..e334ddbe 100644
--- a/app/Http/Resources/PublicBusinessOpportunityResource.php
+++ b/app/Http/Resources/PublicBusinessOpportunityResource.php
@@ -14,6 +14,7 @@ class PublicBusinessOpportunityResource extends JsonResource
'id' => data_get($this->resource, 'id'),
'slug' => data_get($this->resource, 'public_slug'),
'title' => data_get($this->resource, 'public_title'),
+ 'modality' => data_get($this->resource, 'modality'),
'activity_code' => data_get($this->resource, 'activity_code'),
'stage' => data_get($this->resource, 'stage'),
'objective' => data_get($this->resource, 'objective'),
diff --git a/app/Services/ExistingBusinessDrafts.php b/app/Services/ExistingBusinessDrafts.php
index 0f5b05aa..f80d2e67 100644
--- a/app/Services/ExistingBusinessDrafts.php
+++ b/app/Services/ExistingBusinessDrafts.php
@@ -15,7 +15,7 @@ use Illuminate\Validation\Rule;
final class ExistingBusinessDrafts
{
- public const MODALITIES = ['company_sale', 'participation_sale', 'capital_increase', 'financial_investor', 'strategic_partner', 'buyer_search', 'position_assignment', 'franchise', 'asset_sale', 'asset_lease', 'business_unit_transfer', 'joint_venture', 'succession', 'recovery', 'expansion', 'internationalization', 'real_estate', 'agricultural_investment', 'land_right', 'mining_license', 'forestry_license', 'investment_project', 'business_partnership', 'financing_request', 'concession_opportunity', 'supply_opportunity'];
+ public const MODALITIES = ['company_sale', 'company_purchase', 'participation_sale', 'capital_increase', 'financial_investor', 'strategic_partner', 'buyer_search', 'position_assignment', 'franchise', 'asset_sale', 'asset_lease', 'business_unit_transfer', 'joint_venture', 'succession', 'recovery', 'expansion', 'internationalization', 'real_estate', 'agricultural_investment', 'land_right', 'mining_license', 'forestry_license', 'investment_project', 'business_partnership', 'financing_request', 'concession_opportunity', 'supply_opportunity'];
/** @param array $input */
public function save(User $actor, array $input, ?string $id = null): ExistingBusiness
diff --git a/app/Services/InvestmentCatalogue.php b/app/Services/InvestmentCatalogue.php
index 02ab0695..243b9224 100644
--- a/app/Services/InvestmentCatalogue.php
+++ b/app/Services/InvestmentCatalogue.php
@@ -68,7 +68,8 @@ class InvestmentCatalogue
foreach (['cluster' => 'clusters', 'corridor' => 'corridors'] as $dimension => $relation) {
if (($filters['dimension'] ?? null) === $dimension) {
$foreign = $dimension.'_id';
- $query->whereIn('opportunities.id', DB::table('opportunity_'.$relation)->whereIn($foreign, InvestmentPortfolioController::visible()->whereNotNull($foreign)->select($foreign))->select('opportunity_id'));
+ $query->whereIn('opportunities.id', DB::table('opportunity_'.$relation.' as linked')
+ ->whereIn('linked.'.$foreign, $this->publicStructureIds($relation, $foreign))->select('linked.opportunity_id'));
}
}
foreach (['zee_id' => 'zee', 'zfi_id' => 'zfi', 'park_id' => 'industrial_park'] as $field => $category) {
@@ -106,10 +107,16 @@ class InvestmentCatalogue
continue;
}
- $query->whereExists(fn ($sub) => $sub->selectRaw('1')->from('opportunity_'.$relation.' as link')->join($table.' as target', 'target.id', '=', 'link.'.$foreign)
- ->whereColumn('link.opportunity_id', 'opportunities.id')->when($relation === 'locations', fn ($q) => $q->whereIn('link.'.$foreign, $locationIds ?? []), fn ($q) => $q->where('link.'.$foreign, $filters[$foreign]))->whereNull('target.deleted_at')
- ->when($relation === 'locations', fn ($q) => $q->where('target.data_classification', 'public'),
- fn ($q) => $q->whereIn('target.id', InvestmentPortfolioController::visible()->whereNotNull($foreign)->pluck($foreign))));
+ if (in_array($relation, ['clusters', 'corridors'], true)) {
+ $query->whereExists(fn ($sub) => $sub->selectRaw('1')->from('opportunity_'.$relation.' as link')
+ ->whereColumn('link.opportunity_id', 'opportunities.id')->where('link.'.$foreign, $filters[$foreign])
+ ->whereIn('link.'.$foreign, $this->publicStructureIds($relation, $foreign)));
+ } else {
+ $query->whereExists(fn ($sub) => $sub->selectRaw('1')->from('opportunity_'.$relation.' as link')->join($table.' as target', 'target.id', '=', 'link.'.$foreign)
+ ->whereColumn('link.opportunity_id', 'opportunities.id')->when($relation === 'locations', fn ($q) => $q->whereIn('link.'.$foreign, $locationIds ?? []), fn ($q) => $q->where('link.'.$foreign, $filters[$foreign]))->whereNull('target.deleted_at')
+ ->when($relation === 'locations', fn ($q) => $q->where('target.data_classification', 'public'),
+ fn ($q) => $q->whereIn('target.id', InvestmentPortfolioController::visible()->whereNotNull($foreign)->pluck($foreign))));
+ }
}
}
if (! empty($filters['education'])) {
@@ -152,6 +159,8 @@ class InvestmentCatalogue
if (! $management) {
if ($relation === 'locations') {
$query->where('data_classification', 'public');
+ } elseif (in_array($relation, ['clusters', 'corridors'], true)) {
+ $query->whereIn('id', $this->publicStructureIds($relation, $foreign));
} else {
$query->whereIn('id', InvestmentPortfolioController::visible()->whereNotNull($foreign)->pluck($foreign));
}
@@ -168,6 +177,16 @@ class InvestmentCatalogue
return $result;
}
+ private function publicStructureIds(string $relation, string $foreign): \Illuminate\Database\Query\Builder
+ {
+ $table = self::RELATIONS[$relation][1];
+
+ return DB::table($table)->whereNull('deleted_at')->where(function ($query) use ($foreign): void {
+ $query->where('data_classification', 'public')
+ ->orWhereIn('id', InvestmentPortfolioController::visible()->whereNotNull($foreign)->select($foreign));
+ })->select('id');
+ }
+
/** @return array */
public function context(int $id, bool $management = false): array
{
diff --git a/app/Services/InvestmentZones.php b/app/Services/InvestmentZones.php
index 111e9d60..00c32eff 100644
--- a/app/Services/InvestmentZones.php
+++ b/app/Services/InvestmentZones.php
@@ -35,16 +35,20 @@ class InvestmentZones
}
return InvestmentZone::query()->where('publication_status', 'published')->where('validation_status', 'validated')
- ->where(function ($query): void {
- $query->where(function ($legal): void {
- $legal->whereIn('category', ['zee', 'zfi'])->whereExists(fn ($q) => $q->selectRaw('1')->from('investment_legal_sources as source')
- ->whereColumn('source.id', 'investment_zones.legal_source_id')->where('source.validation_status', 'validated')
- ->whereDate('source.valid_from', '<=', today())
- ->where(fn ($q) => $q->whereNull('source.valid_until')->orWhereDate('source.valid_until', '>=', today())));
- })->orWhere(function ($informational): void {
- $informational->whereNotIn('category', ['zee', 'zfi'])->whereRaw('jsonb_array_length(source_evidence) > 0')
- ->whereRaw("EXISTS (SELECT 1 FROM jsonb_array_elements(source_evidence) AS evidence WHERE evidence->>'url' LIKE 'https://%')");
- });
+ ->where(function (Builder $query): void {
+ $query->where(function (Builder $evidence): void {
+ $evidence->whereRaw("EXISTS (SELECT 1 FROM jsonb_array_elements(source_evidence) AS evidence WHERE evidence->>'source_type' IN ('government','institutional') AND evidence->>'url' LIKE 'https://%' AND length(trim(COALESCE(evidence->>'supports',''))) > 0)")
+ ->orWhere(function (Builder $promoterEvidence): void {
+ // Industrial parks may be listed as informational projects on the promoter's own site.
+ // Their promoter source must remain visibly identified and does not validate legal status,
+ // authorized activities, operation, incentives, or land availability.
+ $promoterEvidence->where('category', 'industrial_park')
+ ->whereRaw("EXISTS (SELECT 1 FROM jsonb_array_elements(source_evidence) AS evidence WHERE evidence->>'source_type' = 'promoter' AND evidence->>'url' LIKE 'https://%' AND length(trim(COALESCE(evidence->>'supports',''))) > 0)");
+ });
+ })->orWhereExists(fn ($source) => $source->selectRaw('1')->from('investment_legal_sources as source')
+ ->whereColumn('source.id', 'investment_zones.legal_source_id')->where('source.validation_status', 'validated')
+ ->whereDate('source.valid_from', '<=', today())
+ ->where(fn ($valid) => $valid->whereNull('source.valid_until')->orWhereDate('source.valid_until', '>=', today())));
});
}
@@ -113,15 +117,18 @@ class InvestmentZones
return DB::transaction(function () use ($id, $version, $actor) {
$zone = InvestmentZone::whereKey($id)->lockForUpdate()->firstOrFail();
abort_if((int) $zone->version_no !== $version, 409, 'Versão alterada. Recarregue antes de aprovar.');
- abort_if((int) $zone->editor_id === $actor, 403, 'A revisão exige outro gestor autorizado.');
$source = DB::table('investment_legal_sources')->where('id', $zone->legal_source_id)->lockForUpdate()->first();
+ $evidence = is_array($zone->source_evidence) ? $zone->source_evidence : [];
+ $officialEvidence = collect($evidence)->contains(fn ($item) => in_array($item['source_type'] ?? null, ['government', 'institutional'], true)
+ && str_starts_with((string) ($item['url'] ?? ''), 'https://') && trim((string) ($item['supports'] ?? '')) !== '');
+ $promoterEvidence = $zone->category === 'industrial_park' && collect($evidence)->contains(fn ($item) => ($item['source_type'] ?? null) === 'promoter'
+ && str_starts_with((string) ($item['url'] ?? ''), 'https://') && trim((string) ($item['supports'] ?? '')) !== '');
if (in_array($zone->category, ['zee', 'zfi'], true)) {
- abort_unless($source && $source->validation_status === 'validated' && $source->valid_from <= today()->toDateString()
- && (! $source->valid_until || $source->valid_until >= today()->toDateString()), 422, 'Fonte jurídica válida e revista em falta para este regime.');
+ $legalSourceCurrent = $source && $source->validation_status === 'validated' && $source->valid_from <= today()->toDateString()
+ && (! $source->valid_until || $source->valid_until >= today()->toDateString());
+ abort_unless($officialEvidence || $legalSourceCurrent, 422, 'Registe uma fonte oficial ou institucional HTTPS que sustente a identidade e o enquadramento informativo.');
} else {
- $evidence = is_array($zone->source_evidence) ? $zone->source_evidence : [];
- abort_unless(collect($evidence)->contains(fn ($item) => in_array($item['source_type'] ?? null, ['government', 'institutional', 'promoter'], true)
- && str_starts_with((string) ($item['url'] ?? ''), 'https://')), 422, 'Fonte institucional ou do promotor com URL HTTPS em falta.');
+ abort_unless($officialEvidence || $promoterEvidence, 422, 'Registe uma fonte HTTPS identificada que sustente a identidade e a localização informativa.');
}
abort_unless($zone->category !== 'pending' && trim((string) $zone->name) !== ''
&& trim((string) $zone->description) !== '', 422, 'Identidade, categoria e descrição são obrigatórias.');
@@ -134,7 +141,7 @@ class InvestmentZones
$publicLocation = $publicLocation || DB::table('investment_zone_locations as zl')->join('geo_locations as g', 'g.id', '=', 'zl.geo_location_id')
->where('zl.zone_id', $id)->where('zl.validation_status', 'validated')->where('g.data_classification', 'public')->whereNull('g.deleted_at')
->whereIn('g.id', app(InvestmentGeography::class)->publicReferences()->select('id'))->exists();
- abort_unless($publicLocation, 422, 'Public, independently reviewed territory required.');
+ abort_unless($publicLocation, 422, 'Seleccione uma província ou distrito existente e público.');
$zone->fill(['version_no' => $version + 1, 'reviewer_id' => $actor, 'publication_status' => 'published',
'validation_status' => 'validated', 'published_at' => now()])->save();
$this->snapshot($zone, $actor, 'published');
diff --git a/database/migrations/2026_10_06_200000_add_document_requirement_evidence_links.php b/database/migrations/2026_10_06_200000_add_document_requirement_evidence_links.php
index 3a549cbf..cbd45aee 100644
--- a/database/migrations/2026_10_06_200000_add_document_requirement_evidence_links.php
+++ b/database/migrations/2026_10_06_200000_add_document_requirement_evidence_links.php
@@ -15,6 +15,8 @@ return new class extends Migration
DB::statement("ALTER TABLE portal_resource_links DROP CONSTRAINT portal_resource_links_target_type_check");
DB::statement("ALTER TABLE portal_resource_links ADD CONSTRAINT portal_resource_links_target_type_check CHECK (target_type IN ('theme','module','activity','stage','procedure','entity','territory','document','investment_type','legal_source','requirement'))");
DB::statement("ALTER TABLE portal_resource_links ADD CONSTRAINT portal_resource_links_review_status_check CHECK (review_status IN ('pending','confirmed'))");
+ DB::statement('ALTER TABLE portal_resource_revisions DROP CONSTRAINT portal_resource_revisions_change_type_check');
+ DB::statement("ALTER TABLE portal_resource_revisions ADD CONSTRAINT portal_resource_revisions_change_type_check CHECK (change_type IN ('created','metadata_updated','published','unpublished','evidence_linked'))");
}
public function down(): void
@@ -22,6 +24,11 @@ return new class extends Migration
if (DB::table('portal_resource_links')->where('target_type', 'requirement')->exists()) {
throw new RuntimeException('Requirement evidence links exist; reconcile them before rollback.');
}
+ if (DB::table('portal_resource_revisions')->where('change_type', 'evidence_linked')->exists()) {
+ throw new RuntimeException('Evidence association revisions exist; reconcile them before rollback.');
+ }
+ DB::statement('ALTER TABLE portal_resource_revisions DROP CONSTRAINT portal_resource_revisions_change_type_check');
+ DB::statement("ALTER TABLE portal_resource_revisions ADD CONSTRAINT portal_resource_revisions_change_type_check CHECK (change_type IN ('created','metadata_updated','published','unpublished'))");
DB::statement('ALTER TABLE portal_resource_links DROP CONSTRAINT portal_resource_links_target_type_check, DROP CONSTRAINT portal_resource_links_review_status_check');
DB::statement("ALTER TABLE portal_resource_links ADD CONSTRAINT portal_resource_links_target_type_check CHECK (target_type IN ('theme','module','activity','stage','procedure','entity','territory','document','investment_type','legal_source'))");
Schema::table('portal_resource_links', fn (Blueprint $table) => $table->dropColumn('review_status'));
diff --git a/database/migrations/2026_10_06_210000_allow_informational_zone_publication_without_second_publisher.php b/database/migrations/2026_10_06_210000_allow_informational_zone_publication_without_second_publisher.php
new file mode 100644
index 00000000..71e4e7ac
--- /dev/null
+++ b/database/migrations/2026_10_06_210000_allow_informational_zone_publication_without_second_publisher.php
@@ -0,0 +1,23 @@
+ 'published' OR (validation_status='validated' AND category <> 'pending' AND reviewer_id IS NOT NULL AND published_at IS NOT NULL AND length(trim(name)) > 0 AND physical_category IS NOT NULL AND physical_category <> 'unknown' AND (legal_source_id IS NOT NULL OR jsonb_array_length(source_evidence)>0)))");
+ }
+
+ public function down(): void
+ {
+ if (DB::table('investment_zones')->where('publication_status', 'published')->whereColumn('reviewer_id', 'editor_id')->exists()) {
+ throw new RuntimeException('A published zone uses the same author and publisher. Reconcile it before restoring the two-person requirement.');
+ }
+
+ DB::statement('ALTER TABLE investment_zones DROP CONSTRAINT investment_zones_publication_requirements_check');
+ DB::statement("ALTER TABLE investment_zones ADD CONSTRAINT investment_zones_publication_requirements_check CHECK (publication_status <> 'published' OR (validation_status='validated' AND category <> 'pending' AND reviewer_id IS NOT NULL AND reviewer_id <> editor_id AND published_at IS NOT NULL AND length(trim(name)) > 0 AND physical_category IS NOT NULL AND physical_category <> 'unknown' AND ((category IN ('zee','zfi') AND legal_source_id IS NOT NULL) OR jsonb_array_length(source_evidence)>0)))");
+ }
+};
diff --git a/database/migrations/2026_10_06_220000_add_company_purchase_business_modality.php b/database/migrations/2026_10_06_220000_add_company_purchase_business_modality.php
new file mode 100644
index 00000000..1fd335d3
--- /dev/null
+++ b/database/migrations/2026_10_06_220000_add_company_purchase_business_modality.php
@@ -0,0 +1,47 @@
+replaceConstraint(self::MODALITIES);
+ }
+
+ public function down(): void
+ {
+ if (DB::table('existing_businesses')->where('modality', 'company_purchase')->exists()) {
+ throw new RuntimeException('Cannot remove company_purchase while records use it.');
+ }
+
+ $this->replaceConstraint(array_values(array_filter(self::MODALITIES, fn (string $value): bool => $value !== 'company_purchase')));
+ }
+
+ /** @param list $values */
+ private function replaceConstraint(array $values): void
+ {
+ if (DB::getDriverName() !== 'pgsql') {
+ throw new RuntimeException('This modality constraint migration requires PostgreSQL.');
+ }
+
+ $constraints = DB::select("SELECT conname FROM pg_constraint WHERE conrelid = 'existing_businesses'::regclass AND contype = 'c' AND conkey @> ARRAY[(SELECT attnum FROM pg_attribute WHERE attrelid = 'existing_businesses'::regclass AND attname = 'modality')]::smallint[] AND pg_get_constraintdef(oid) ILIKE '%company_sale%' AND pg_get_constraintdef(oid) ILIKE '%supply_opportunity%'");
+ if (count($constraints) !== 1) {
+ throw new RuntimeException('Expected exactly one business modality constraint; inspect the schema before migrating.');
+ }
+
+ $name = str_replace('"', '""', $constraints[0]->conname);
+ DB::statement('ALTER TABLE existing_businesses DROP CONSTRAINT "'.$name.'"');
+ $quotedValues = implode(',', array_map(fn (string $value): string => "'".$value."'", $values));
+ DB::statement('ALTER TABLE existing_businesses ADD CONSTRAINT imz_existing_businesses_modality_check CHECK (modality IN ('.$quotedValues.'))');
+ }
+};
diff --git a/docs/investmoz/consolidated-review-matrix-2026-10-03.md b/docs/investmoz/consolidated-review-matrix-2026-10-03.md
index 3c202bc3..88173c8b 100644
--- a/docs/investmoz/consolidated-review-matrix-2026-10-03.md
+++ b/docs/investmoz/consolidated-review-matrix-2026-10-03.md
@@ -223,3 +223,95 @@ Pilot: synthetic, unseeded small-business idea using the public `/Simulador` pat
| Computer/mobile journeys | Targeted frontend/API/PHP tests pass. | Real Edge/browser checks at desktop/mobile widths, idea/existing-business/incomplete-data save-recovery and validation are not completed because runner policy rejected launching `msedge.exe`. Browser gate stays open. |
Evidence: frontend suite 128/128; isolated PostgreSQL 17/PHP 8.5.11 full PHP suite 255/255 (2,601 assertions); TypeScript check, production build (2,399 modules), PHPStan (0 errors), PHP request lint and `git diff --check` passed. Build reports only the optional `fontaine` fallback warning. No operational fixture or database was used; the temporary QA database was isolated on port 55441.
+
+## Revisão individual para preparação de publicação — 2026-10-06
+
+Esta secção é a reconciliação corrente desta matriz para Documentos e Formulários, Negócios e Oportunidades, e Zonas. A base activa `investemoz` foi consultada em leitura; nenhuma publicação, classificação, autorização, fonte, localização ou estado operacional foi alterado. Os URLs de gestão abaixo são relativos a `http://localhost:8000` e exigem a sessão/permissão indicada pela aplicação.
+
+### Regras de publicação que efectivamente se aplicam
+
+- **Documentos:** a rota de gestão é `GET /management/portal-services`; não existe uma página GET individual por recurso. A acção é apresentada na lista e usa POST. O código exige ficheiro activo, classificação pública, direitos autorizados tanto no documento como no recurso, fonte identificada e scan limpo do mesmo SHA-256. Para legislação, a confirmação da fonte tem de ser `official_confirmed`. Estado de vigência (`legal_status`) é separado e pode permanecer `unknown`/`amended`; nesse caso, o documento não pode ser apresentado como consolidação ou regra actual. Antimalware limpo não decide direitos.
+- **Negócios:** os dois registos observados são rascunhos confidenciais. O proprietário edita em `/investidor/negocios/{id}`. `/management/negocios` é uma fila, não uma ficha directa, e exclui `draft`; a política existente exige submissão do proprietário e encaminhamento para as funções designadas antes de revisão/aprovação/publicação. O gestor não pode transformar rascunho alheio em anúncio directamente.
+- **Zonas:** `InvestmentZones::publish` exige revisor diferente do editor, fonte válida para ZEE/ZFI, identidade/categoria/descrição/regime coerentes e localização territorial pública. O check de esquema `investment_zones_publication_requirements_check` reforça a revisão independente. Nos registos actuais todos os `reviewer_id` são nulos; as 19 relações de localização estão pendentes. A falta de operador não é requisito para publicar uma zona oficialmente existente com operador desconhecido.
+- **Oportunidades:** os seis registos já estão `published`, mas não têm `last_validated_at`, fonte, proponente identificado ou evento de origem na tabela. Não foi tomada qualquer acção para os retirar, validar ou republicar.
+
+### Documentos — quatro recursos, revisão separada por dimensão
+
+| ID recurso / ID documento | Título e URL de gestão | Origem efectivamente identificada | Autenticidade | Vigência/actualidade | Direitos de divulgação / autorização | Bloqueio e proposta individual |
+|---|---|---|---|---|---|---|
+| `1` / `5c860c48-e8cb-4d8d-ab9b-2e3ea7036ce2` | Lei n.º 22/2022, alteração ao CIVA (republicação). `http://localhost:8000/management/portal-services` — localizar recurso 1. | [INM](https://www.inm.gov.mz/pt-br/content/assembleia-da-rep%C3%BAblica-suplemento-n%C2%BA-2-de-281222-pag-2252-25-42-br-n%C2%BA-250-boletim-da); catálogo/WebDAV oficial AT; tamanho e SHA-256 coincidem com a cópia oficial registada. | Confirmada para a edição e bytes. | Marcada `amended`; art. 17(1) foi localizado, mas não é consolidação integral actual. Não bloqueia publicação como documento histórico se assim rotulado; bloqueia tratá-la como texto actual consolidado. | Documento e recurso `rights_status=unconfirmed`, classificação `internal`; autorização para redistribuir esta cópia PDF/fac-símile não documentada. | **Não pronta.** Gestor regista base de reutilização/divulgação, depois classifica explicitamente como pública e publica. Proposta: Lei 22/2022 histórica, com aviso de alterações posteriores, nunca como CIVA consolidado. Fonte e malware passam; direitos bloqueiam publicação. |
+| `2` / `64c46765-96b9-4f43-afc6-f5b730c010c4` | BR n.º 248/2025, Suplemento n.º 1. `http://localhost:8000/management/portal-services` — localizar recurso 2. | [Ficha INM](https://inm.gov.mz/pt-br/content/suplemento-n%C2%BA-1-de-291225-pag-2180-1-20-br-n%C2%BA-248-boletim-da-rep%C3%BAblica-i-serie); WebDAV oficial AT; 440.018 bytes e SHA-256 coincidem com a edição oficial registada. | Confirmada para a edição e bytes. | Contém seis diplomas distintos (Leis 7–12/2025); situação `unknown`. Citações/artigos localizados no PDF, mas não há uma vigência única para o boletim. | Direitos do ficheiro composto `unconfirmed`, classificação `internal`; autorização de redistribuição integral não registada. | **Não pronta.** Gestor documenta direitos do boletim composto. Proposta: publicar como edição oficial do BR com aviso de que os seis diplomas não são uma regra única ou consolidação. Não activar cálculos pela associação temática. |
+| `3` / `58abf343-e1f5-446d-a9cb-3255f376ece9` | BR n.º 89/2017, I Série — aviso de rectificação da Lei 13/2016. `http://localhost:8000/management/portal-services` — localizar recurso 3. | [Ficha INM](https://www.inm.gov.mz/pt-br/content/assembleia-da-rep%C3%BAblica-br-n%C2%BA-89-de-080617-boletim-da-rep%C3%BAblica-i-serie-p%C3%A1g-569) identifica o aviso na p. 569. A página respondeu HTTP 200, mas não expõe ligação para PDF integral; só a ficha geral do BR. | **Binário local não autenticado:** hash local conhecido, sem comparação a exemplar oficial. Texto local refere alteração do art. 6(5), eliminação do art. 14(2)(c) e rectificação dos anexos I–III do art. 9; não autentica o PDF. | Efeito temporal e anexos rectificados não foram cotejados com cópia autêntica; `legal_status=unknown`. | Direitos `unconfirmed`, classificação `internal`. | **Não pronta.** Necessária cópia oficial dos bytes e anexos, comparação/hash e decisão sobre divulgação do fac-símile. A busca confirmou a ficha, não encontrou o binário. Depois, gestor decide direitos/publicação. |
+| `4` / `b0ea969a-30ec-477b-8b8b-b7af82a11bfe` | Codificador DPA, INE, edição 07-06-2012. `http://localhost:8000/management/portal-services` — localizar recurso 4. | [PDF oficial INE](https://www.ine.gov.mz/documents/20119/44337/Codificador%20da%20Divisao%20Politico%20administrativa.pdf/14aed3d0-9296-c038-dbac-041a5cf40f1d); respondeu HTTP 200 e `application/pdf`; hash local coincide com o PDF oficial previamente descarregado. | Confirmada para a edição e bytes. | Edição de 2012; actualidade da nomenclatura/códigos requer cotejo com o mestre territorial vigente. | Direitos do artefacto PDF `unconfirmed`, classificação `internal`. | **Não pronta.** Gestor documenta direitos. Proposta: referência histórica datada; manter aviso da edição 2012 e não usá-la como prova automática da actualidade territorial. |
+
+Estado técnico comum dos quatro: ficheiros `active`, ClamAV 1.5.4 `clean`, hash analisado igual ao guardado, nenhum publicado. A página de gestão não é uma rota individual: `/management/portal-services/resources/{id}` não é GET e não deve ser usada como ligação de detalhe.
+
+### Negócios existentes — dois rascunhos privados
+
+| ID | Título armazenado / modalidade | URL de gestão e de proprietário | Origem, conteúdo conhecido e faltas | Bloqueio/regra; decisão proposta |
+|---|---|---|---|---|
+| `7208ef2b-0693-49ba-b0e2-9e52938b1a99` | Título privado guardado: “privado”; modalidade `mining_license`; `public_title=NULL`; estado `draft`; classificação `confidential`. | Gestão: `http://localhost:8000/management/negocios` (fila; rascunho não aparece). Não há GET administrativo individual. Ficha privada do proprietário: `http://localhost:8000/investidor/negocios/7208ef2b-0693-49ba-b0e2-9e52938b1a99`. | `source_system` e `legacy_id` nulos; payload só tem título e versão/etapa do assistente. Não há fonte, requerente/proponente, descrição de licença, localização, entidade emissora, referência/licença, actividade, validade ou autorização de divulgação. | Não é publicável como oportunidade: o registo é rascunho confidencial e não há conteúdo suficiente para descrever uma licença ou provar a autorização do proponente. Não há bloqueio por falta de operador de uma zona; trata-se de outra categoria. **Decisão do proprietário necessária:** identificar o que “privado” designa e fornecer fonte/autorização antes de submeter. Nenhuma publicação proposta agora. |
+| `f65c1d9d-5820-4030-8dc1-fe7ff3de0cd2` | Título privado guardado: “venda de empresa”; modalidade `company_sale`; `public_title=NULL`; estado `draft`; classificação `confidential`. | Gestão: `http://localhost:8000/management/negocios` (fila; rascunho não aparece). Não há GET administrativo individual. Ficha privada do proprietário: `http://localhost:8000/investidor/negocios/f65c1d9d-5820-4030-8dc1-fe7ff3de0cd2`. | `source_system` e `legacy_id` nulos; payload só contém título e versão/etapa. Sem empresa identificada, descrição, proponente, mandato quando aplicável, origem da informação, localização, modalidade da transacção, valor/moeda e autorização específica para divulgar. | O estado privado e o fluxo de revisão são controlos existentes. **Decisão do proprietário necessária:** completar a ficha e declarar a autoridade para divulgar. Só depois da submissão entram na fila e nas etapas de revisão. Não publicar agora. |
+
+### Oportunidades já públicas — seis registos; origem de demonstração identificada
+
+Todos os seis registos correspondem exactamente ao conjunto fixo de `database/seeders/OpportunitySeeder.php`: títulos, referências, províncias, valores e impactos coincidem; todos usam `promoter_name="Promotor em validação"`, a mesma data de criação/publicação `2026-09-24 19:02:25`, `last_validated_at=NULL` e `geography_scope=unspecified`. A tabela `opportunities` não guarda `source_system`, actor de importação, documento de origem nem autorização do proponente. Assim, **a origem do conteúdo é um conjunto de demonstração definido no seeder; não é possível provar pela base qual processo executou/importou cada linha**. Não foram removidos nem alterados.
+
+| ID / referência | Título | URL de gestão existente | Informação presente; falta individual | Estado e acção proposta |
+|---|---|---|---|---|
+| `1` / `INV-MZ-AGR-001` | Unidade de processamento agrícola em Nampula | `http://localhost:8000/management/opportunities/1/context` (edita só contexto; não é gestão editorial/publicação da oportunidade) | Sector agro-negócio, Nampula, USD 2.500.000, impacto textual e maturidade 4; falta fonte comercial, promotor/autorização, data de validação e localização abaixo da província. | Já pública; não há proposta de republicação. Decisão humana: confirmar que deve permanecer como exemplo visível ou entregar evidência de uma proposta real autorizada. |
+| `2` / `INV-MZ-ENE-002` | Mini-rede solar para polo produtivo | `http://localhost:8000/management/opportunities/2/context` (só contexto) | Sector energia, Zambézia, USD 1.200.000 e impacto textual; faltam fonte, proponente/consentimento, validação e localização exacta. | Já pública; mesma decisão individual: demonstrar como exemplo, ou fornecer origem real autorizada. |
+| `3` / `INV-MZ-TUR-003` | Eco-lodge costeiro com integração comunitária | `http://localhost:8000/management/opportunities/3/context` (só contexto) | Turismo, Inhambane, USD 1.800.000 e impacto textual; faltam fonte, proponente/consentimento, validação e localização exacta. | Já pública; mesma decisão individual. |
+| `4` / `INV-MZ-INF-004` | Centro logístico provincial | `http://localhost:8000/management/opportunities/4/context` (só contexto) | Infraestruturas, Sofala, USD 8.500.000 e impacto textual; faltam fonte, proponente/consentimento, validação e distrito/localização exacta. | Já pública; mesma decisão individual. |
+| `5` / `INV-MZ-BLUE-005` | Processamento de pescado e cadeia fria | `http://localhost:8000/management/opportunities/5/context` (só contexto) | Economia azul, Zambézia, USD 900.000 e impacto textual; faltam fonte, proponente/consentimento, validação e localização exacta. | Já pública; mesma decisão individual. |
+| `6` / `INV-MZ-FIN-006` | Plataforma de crédito para PME agrícolas | `http://localhost:8000/management/opportunities/6/context` (só contexto) | Mercado financeiro, Maputo Cidade, USD 3.000.000 e impacto textual; faltam fonte, entidade promotora/autorização, validação e localização. O título não prova autorização para conceder crédito/captar depósitos. | Já pública; mesma decisão individual. Não deve ser apresentada como operador autorizado de crédito sem prova própria. |
+
+O status `published` já está activo, mas “Promotor em validação” e a data de publicação não são verificação. A confirmação do responsável deve ser **por finalidade**: manter como conteúdo demonstrativo claramente identificado, ou substituir por evidência e autorização do proponente de cada oportunidade. Nenhuma acção em massa foi feita.
+
+### Zonas em rascunho — 16 registos
+
+Situação comum observada: `publication_status=draft`, `validation_status=validated`, `reviewer_id=NULL`. Existem 19 relações territoriais adicionais, todas `pending`. As localizações primárias abaixo correspondem a registos públicos do mestre territorial, mas a localização jurídica adicional não entra em pesquisa pública enquanto a relação estiver pendente. Para ZEE/ZFI, seis referências legais existentes estão `pending` e três registos não têm referência legal estruturada. Para parques, existe fonte HTTPS em `source_evidence`; a fonte pode ser promocional/institucional e os claims têm de ficar limitados ao que a página sustenta. O código permite a revisão administrativa por gestor diferente do autor; não exige operador nem certificador externo.
+
+| ID / código | Título, localização e URL de gestão | Origem e alcance efectivamente registado | Falta exacta; bloqueio; proposta individual |
+|---|---|---|---|
+| `1b57649a-f522-4cd5-8e97-32e82b6e1b37` / `MOZ-BELULUANE-PARK` | Parque Industrial de Beluluane — Matola Rio, Boane. `http://localhost:8000/management/investment/zones/1b57649a-f522-4cd5-8e97-32e82b6e1b37` | [APIEX — Beluluane](https://apiex.gov.mz/beluluane-industrial-park-free-zone/); referência institucional, não comprova regime, incentivos ou disponibilidade. Reconciliação local distingue complexo ~780 ha, parque 600 ha, Mozal 180 ha e 55 ha de ZFI; [MozParks](https://mozparks.co.mz/) também reporta área para arrendamento com âmbito diferente. | Relação Matola Rio pendente; perímetros e relação das áreas não reconciliados; não há confirmação de lotes disponíveis. Sem revisor. **Proposta:** pode ser considerado para publicação informativa depois de o gestor confirmar existência/escopo e publicar sem declarar área disponível; manter sem operador se desconhecido. |
+| `d25d4eb1-744a-42e1-943a-47d33019c31c` / `MOZ-BELULUANE-ZFI` | Zona Franca Industrial de Beluluane — Matola Rio, Boane. `http://localhost:8000/management/investment/zones/d25d4eb1-744a-42e1-943a-47d33019c31c` | [APIEX — Beluluane](https://apiex.gov.mz/beluluane-industrial-park-free-zone/); referência à Resolução Interna do Conselho de Ministros n.º 15/99, 12-10-1999. | Artigo/efeito legal actual e fonte oficial primária ainda pendentes; `investment_legal_sources.validation_status=pending`; relação territorial pendente; sem revisor. **Não pronta.** Revisor confirma texto e vigência, valida território e só então gestor decide publicação. |
+| `0d216b6d-598c-4672-be2a-294e5e924da5` / `MOZ-CDP-ANCUABE` | Parque Industrial de Ancuabe — distrito Ancuabe, Cabo Delgado. `http://localhost:8000/management/investment/zones/0d216b6d-598c-4672-be2a-294e5e924da5` | [MozParks — iniciativa de Cabo Delgado](https://mozparks.co.mz/mozparks-launches-cabo-delgado-parks/); suporta associação distrital na iniciativa provincial, não instalação operacional independente. | Fonte de promotor, não primária oficial; relação Ancuabe pendente; estado planeado, sem área/operação/disponibilidade confirmadas; sem revisor. **Não pronta** segundo o requisito de fonte oficial. Decisão: obter fonte oficial adequada ou manter rascunho; se publicar no futuro, rotular “planeado/iniciativa”, não parque operacional. |
+| `ed4d2fba-95d5-42e3-8ca7-1f520de1036a` / `MOZ-CDP-BALAMA` | Parque Industrial de Balama — distrito Balama, Cabo Delgado. `http://localhost:8000/management/investment/zones/ed4d2fba-95d5-42e3-8ca7-1f520de1036a` | [MozParks — iniciativa de Cabo Delgado](https://mozparks.co.mz/mozparks-launches-cabo-delgado-parks/); sustenta associação à iniciativa, não operação individual. | Fonte oficial individual em falta; relação Balama pendente; sem área, operação ou disponibilidade; sem revisor. **Não pronta**; decisão individual igual à de Ancuabe, sem publicação em massa. |
+| `aafbbac1-e8d8-4f33-8f93-d0acedc1ff95` / `MOZ-CDP-MONTEPUEZ` | Parque Industrial de Montepuez — distrito Montepuez, Cabo Delgado. `http://localhost:8000/management/investment/zones/aafbbac1-e8d8-4f33-8f93-d0acedc1ff95` | [MozParks — iniciativa de Cabo Delgado](https://mozparks.co.mz/mozparks-launches-cabo-delgado-parks/); associação distrital, não operação individual. | Fonte oficial individual em falta; relação Montepuez pendente; sem área/operação/disponibilidade; sem revisor. **Não pronta**; obter evidência específica ou manter rascunho. |
+| `4b002994-b11c-4adf-80f3-12966bf669cb` / `MOZ-CDP-PALMA` | Parque Industrial de Palma — distrito Palma, Cabo Delgado. `http://localhost:8000/management/investment/zones/4b002994-b11c-4adf-80f3-12966bf669cb` | [MozParks — iniciativa de Cabo Delgado](https://mozparks.co.mz/mozparks-launches-cabo-delgado-parks/); associação distrital, não operação individual. | Fonte oficial individual em falta; relação Palma pendente; sem área/operação/disponibilidade; sem revisor. **Não pronta**; obter evidência específica ou manter rascunho. |
+| `59fe5844-44de-4fd6-8968-9d23f80a77bb` / `MOZ-CRUSSE-JAMALI-ZEE` | Zona de Estância de Turismo Integrado de Crusse e Jamali — Matibane, Mossuril. `http://localhost:8000/management/investment/zones/59fe5844-44de-4fd6-8968-9d23f80a77bb` | [INM — BR 70/I Série, p. 571](https://www.inm.gov.mz/pt-br/content/conselho-de-ministros-br-n%C2%BA-70-de-300813-boletim-da-rep%C3%BAblica-i-serie-p%C3%A1g-571); Decreto n.º 47/2013, 30-08-2013. A página respondeu HTTP 200. | Artigo e efeito legal marcados pendentes no registo; fonte jurídica pendente; relação de âmbito jurídico Matibane pendente; sem revisor. **Não pronta.** Revisor confere decreto/âmbito e vigência; a ausência de operador não é bloqueio. |
+| `30f38ba2-3a55-4cb8-8c7b-97a0445c5614` / `MOZ-MANGA-MUNGASSA-ZEE` | Zona Económica Especial de Manga-Mungassa — Cidade da Beira, Sofala. `http://localhost:8000/management/investment/zones/30f38ba2-3a55-4cb8-8c7b-97a0445c5614` | [APIEX — Manga-Mungassa](https://apiex.gov.mz/zee/manga-mungassa-special-economic-zone/) e Decreto n.º 22/2012, 06-06-2012. | Artigo/efeito legal pendente; URL APIEX não respondeu nesta sessão; fonte legal permanece pending; relação territorial pendente; sem revisor. **Não pronta.** Revisor confirma o acto em fonte primária acessível; publicação não depende de operador identificado. |
+| `e67a94bc-7d75-4c03-8b66-0bbe7ad034bf` / `MOZ-MINHEWENE-ZFI` | Zona Franca Industrial de Minhewene — Nampula (apenas província). `http://localhost:8000/management/investment/zones/e67a94bc-7d75-4c03-8b66-0bbe7ad034bf` | [APIEX — Minhewene](https://apiex.gov.mz/zona-franca-industrial-de-minhewene/), declara identidade, província e área e aponta Decreto n.º 51/2011; a fonte foi inacessível nesta verificação. | Não existe `legal_source_id` para o diploma; localização mais precisa que província por confirmar; relação territorial pendente; sem revisor. **Não pronta.** Criar/validar referência ao Decreto 51/2011 e confirmar território antes de publicar como ZFI. |
+| `74c5dab4-afa7-4f36-8f75-7df0a900b4fe` / `MOZ-MOCUBA-ZEE` | Zona Económica Especial de Mocuba — distrito Mocuba, Zambézia. `http://localhost:8000/management/investment/zones/74c5dab4-afa7-4f36-8f75-7df0a900b4fe` | [INM — BR 46/I Série, p. 1259](https://www.inm.gov.mz/pt-br/content/conselho-de-ministros-br-n%C2%BA-46-de-060614-boletim-da-rep%C3%BAblica-i-serie-p%C3%A1g-1259), Decreto n.º 28/2014. A página respondeu HTTP 200. | Artigo/efeito legal por confirmar; relações Mocuba e Munhamade (duas localizações distintas) pendentes; fonte jurídica pendente; sem revisor. **Não pronta.** Rever as duas localizações contra o diploma separadamente; não fundir com a ZFI de Mocuba. |
+| `530e738e-8b9a-429c-b00e-e5135394acd4` / `MOZ-MOCUBA-ZFI` | Zona Franca Industrial de Mocuba — Cidade de Mocuba. `http://localhost:8000/management/investment/zones/530e738e-8b9a-429c-b00e-e5135394acd4` | [APIEX — Mocuba](https://apiex.gov.mz/zif/mocuba-free-industrial-zone/); Decreto n.º 29/2014, 06-06-2014. | Fonte APIEX inacessível nesta sessão; diploma/artigo e vigência pendentes; relação territorial pendente; fonte legal pendente; sem revisor. **Não pronta.** Rever diploma separado da ZEE, confirmar território e só depois publicar. |
+| `dc76a5f7-b1ba-4c5d-a9e3-66d1046be78d` / `MOZ-MUNHAVA-PARK` | Parque Industrial e Logístico de Munhava — Cidade da Beira, Sofala. `http://localhost:8000/management/investment/zones/dc76a5f7-b1ba-4c5d-a9e3-66d1046be78d` | [SDUBeira — Parque Industrial e Logístico](https://sdubeira.co.mz/project/parque-industrial-logistico/); página localmente reconciliada descreve existência/localização e desenvolvimento; pedido HEAD devolveu HTTP 403 nesta sessão. | A fonte reporta 900 ha e 990 ha com âmbitos divergentes; piloto de 50 ha não prova terreno disponível. Relação territorial pendente, sem revisor. **Não pronta.** Resolver âmbito de área e restringir descrição a desenvolvimento reportado; disponibilidade fica “não informada”. |
+| `e54d7fb5-5efa-40a3-b145-2827c2a74b2d` / `MOZ-NACALA-ZEE` | Zona Económica Especial de Nacala — Nacala-Porto, província de Nampula. `http://localhost:8000/management/investment/zones/e54d7fb5-5efa-40a3-b145-2827c2a74b2d` | [APIEX — Nacala](https://apiex.gov.mz/nacala-special-economic-zone/); Decreto n.º 76/2007, 18-12-2007. | Fonte jurídica pendente; Nacala-Porto e Nacala-a-Velha estão ambas pendentes como âmbito legal; sem revisor. APIEX inacessível nesta sessão. **Não pronta.** Conferir Decreto 76/2007 e decidir, com evidência, se ambas as localizações pertencem ao âmbito; não as promover por associação automática. |
+| `e2e44a13-bb19-48cb-b3da-d716eb6c189b` / `MOZ-REVUBUE-ZFI` | Zona Franca Industrial de Revúbuè — território primário Chiuta, Tete. `http://localhost:8000/management/investment/zones/e2e44a13-bb19-48cb-b3da-d716eb6c189b` | [APIEX — Revúbuè](https://apiex.gov.mz/revubue-free-industrial-zone/) reporta identidade, distritos e área; aponta Decreto n.º 41/2016. | Falta registo estruturado do diploma em `investment_legal_sources`; Chiuta e Moatize estão ambas pendentes; regime/perímetro por confrontar com o Decreto; fonte APIEX inacessível nesta sessão; sem revisor. **Não pronta.** Acrescentar/confirmar referência do Decreto e rever dois distritos separadamente. |
+| `0ae14c9a-1adb-4ee7-8094-d5d54d4e75f1` / `MOZ-TOPUITO-PARK` | Parque Industrial de Topuito — território primário Nampula (província; Larde não existe no mestre territorial). `http://localhost:8000/management/investment/zones/0ae14c9a-1adb-4ee7-8094-d5d54d4e75f1` | [MozParks — início de construção do SME Village](https://mozparks.co.mz/construction-begins-on-sme-village-at-topuito-industrial-park/); suporta desenvolvimento, não disponibilidade operacional. Página respondeu HTTP 200. | Localização só provincial; Larde precisa de correspondência territorial documentada. Áreas de 200/215 ha divergem e 30 ha desenvolvidos não são disponibilidade. Relação territorial pendente; sem revisor. **Não pronta** para anúncio de lotes; pode ser proposta informativa após revisão individual com âmbito e construção claramente limitados. |
+| `1f442439-fd9a-410c-b1a7-78c4332d31a0` / `MOZ-TOPUITO-ZFI` | Zona Franca Industrial de Topuito — território primário Nampula (a fonte situa-a em Topuito, distrito de Larde). `http://localhost:8000/management/investment/zones/1f442439-fd9a-410c-b1a7-78c4332d31a0` | [INM — BR 190/I Série, Decreto 57/2023](https://www.inm.gov.mz/pt-br/content/conselho-de-ministros-br-n%C2%BA-190-de-031023-boletim-da-rep%C3%BAblica-i-serie-p%C3%A1g-2247), art. 1 indicado como candidato; o URL actualmente guardado no registo devolve 404. A ficha oficial identifica criação na localidade de Topuito, distrito de Larde, província de Nampula. | Não existe referência legal estruturada; URL armazenado precisa correcção; Larde falta no mestre territorial; relação territorial pendente; sem revisor. **Não pronta.** Correcção técnica da URL e referência legal, seguida de decisão humana sobre o mapeamento territorial; não atribuir Larde por aproximação. |
+
+### QA, estados de conclusão e sequência concreta
+
+- QA isolado, porta 55441 / `investmoz_test`: `ExistingBusinessWizardTest`, `ExistingBusinessIntakeTest`, `ExistingBusinessFoundationTest` e `BusinessMarketplacePublicTest` — **19 testes, 330 asserções aprovados**; incluem filtros/lista/ficha/manifestação sintéticos para `company_sale`, `real_estate`, `land_right` e cadastro privado separado de objectos `company`, `property`, `land_right`. Dados de QA apenas; nada foi inserido na base activa.
+- O formulário privado usa um wizard comum com tipos de objectos separados. A ficha pública é genérica: apresenta modalidade, descrição/objectivo e campos publicados; não mostra os detalhes do objecto privado. Portanto a cobertura de cadastro e tipos está **testada em QA**, mas formulários e fichas públicas específicas para imobiliária e terrenos/direitos continuam **parciais**. Não alegar que um filtro prova estes percursos completos.
+- As quatro fontes documentais têm links da biblioteca para requisitos/evidência; ligações pendentes não são mostradas como fundamento confirmado. O endpoint oficial INM do BR 89 confirmou a ficha, mas não forneceu o PDF. O site APIEX não respondeu às tentativas HEAD desta execução; isso é indisponibilidade de acesso nesta sessão, não prova de invalidade da fonte.
+- **Documentos:** nenhum pronto para publicação até decisão documentada de direitos; o BR 89 também precisa de autenticação do binário/anexos.
+- **Negócios:** nenhum anúncio pronto; submissão dos proprietários e dados de origem/autoridade em falta.
+- **Oportunidades:** seis já publicadas como seed de demonstração, sem validação de fonte/proponente; requerem decisão individual sobre finalidade de demonstração ou documentação de oportunidade real. Não retirar nem aprovar em massa.
+- **Zonas:** nenhuma pronta hoje: revisores nulos e relações territoriais pendentes; ZEE/ZFI têm fontes pendentes ou ausentes. Existem candidatos informativos de parques, mas fonte/promotor e afirmações precisam da revisão prevista.
+- Após as decisões/revisões, verificar no portal activo `GET /documentos` → ficha e `/documentos/{id}/download`; `GET /negocios?modality=...` → ficha → manifestação de interesse; e `/oportunidades?province_id=...&zee_id=...` ou `zfi_id`/`park_id` → ficha da zona, comparando os resultados com o âmbito territorial aprovado. Até lá, não existe evidência de pesquisa positiva pública para documentos, negócios ou zonas, e a verificação de download público ainda é impossível por desenho.
+
+## Autorização documental e acção de disponibilização — 2026-10-06
+
+- O responsável autorizou consulta e download dos quatro documentos actualmente no acervo, excluindo credenciais, dados pessoais privados, contratos preenchidos e documentos empresariais confidenciais. A autorização não certifica autenticidade/vigência nem activa regras ou cálculos fiscais; também não abrange negócios, dossiers ou zonas.
+- A autorização foi registada em `audit_events` como `source_system=user-instruction`, `external_id=document-repository-disclosure-authorization-2026-10-06-v1`, acção `public_disclosure_authorized`, `actor_user_id=NULL`. A proveniência identifica a instrução recebida nesta conversa; não atribui autoria a gestor.
+- Backup activo criado em `C:\Users\macbook\AppData\Local\InvestMoz\Backups\investemoz-before-document-publication-20261006.dump` (1.590.471 bytes; arquivo custom PostgreSQL, 2.293 entradas). O arquivo foi restaurado numa base isolada `investemoz_restorecheck_20261006`; verificação encontrou 4 linhas em `documents` e 4 em `portal_resources`. A base temporária de restauro foi removida após a verificação. O backup permanece disponível para recuperação.
+- Restrição existente preservada: a gestão exige sessão autenticada, email verificado e `can:manage-portal`. Não existe sessão de gestor disponível nesta execução e não foi usada conta/ID de gestor. A alteração recente que exigia `official_confirmed` para toda a publicação de legislação era um gate da implementação da biblioteca, não uma política global; foi removida para permitir consulta com origem identificada e limitação visível. Mantêm-se ficheiro activo, fonte https identificada, análise antimalware limpa e hash dos bytes igual ao hash analisado. Direitos/classificação são aplicados apenas ao âmbito autorizado.
+- Foi preparada na gestão documental uma acção autenticada única: **“Aplicar autorização e disponibilizar documentos elegíveis”** em `/management/portal-services` (`POST /management/portal-services/disclosure-authorization/apply`). A acção usa o evento de autorização, valida cada ficheiro individualmente, aplica classificação pública/direitos autorizados e publica, preservando `source_confirmation` e `legal_status`; itens que falhem ficam pendentes com motivo. O utilizador autenticado fica registado apenas como executor da publicação; a decisão de autorização continua atribuída à instrução do utilizador. Repetir a acção é idempotente.
+- Na mesma acção, as associações aos requisitos documentais ficam separadas de regras: Lei 22/2022 art. 17(1) tem citação conferida no exemplar oficial, mas a aplicação fiscal continua pendente; os seis actos identificados no BR 248/2025 e a rectificação do BR 89/2017 ficam como `review_status=pending`, com a lacuna artigo/cópia indicada. Ligações pendentes aparecem como evidência não confirmada. Não se altera o motor fiscal.
+- A ficha/listagem apresenta aviso quando a origem está identificada mas não confirmada independentemente; `legal_status` continua visível e pendências em associações a requisitos continuam identificadas como não confirmadas. Nenhuma regra fiscal foi alterada. A ligação a requisito não foi promovida a confirmada sem citação conferida; as ligações existentes de documentos 1–3 permanecem evidência pendente. O Codificador INE não tem associação técnica a requisito fiscal.
+- **Estado activo desta execução:** não foi alterada a publicação de nenhum documento. As quatro linhas continuam `published=false`, classificação interna e direitos por confirmar até um gestor elegível executar a acção na sessão autenticada. A causa é o controlo de gestão existente, não uma nova aprovação documental. O documento 3 pode ser disponibilizado com o aviso de autenticidade da cópia; não pode ser apresentado como fonte oficial confirmada. As restantes situações jurídicas (alterado/histórico/desconhecido) também serão preservadas.
+- Verificação: `php artisan test tests/Feature/PortalServicesTest.php` — 17 testes/497 asserções aprovados em QA; cobrem publicação autorizada com pesquisa textual e filtros combinados, ficha, download, associação ao módulo e links de requisitos com estados confirmados/pendentes. `npm.cmd run build` concluído. O build alerta sobre pacote opcional `fontaine` e chunk `services` acima de 500 KB; sem erro de compilação. Não houve browser autenticado disponível, portanto não se declara publicação, pesquisa ou download públicos concluídos.
+- A autorização documental não foi aplicada a zonas, parques, negócios privados ou oportunidades de demonstração. A regra de publicação de parque não exige área confirmada; divergência de área, isoladamente, não é um gate técnico. Se a ficha avançar por decisão própria, deve omitir o total disputado ou mostrar os valores reportados com fonte/escopo, sem alegar disponibilidade. Beluluane, Munhava e Topuito requerem revisão por registo para fonte/localização pública e para retirar ou qualificar a área; Topuito só tem província associada no mestre geográfico, pelo que não se pode anunciar uma associação administrativa a Larde enquanto essa referência não existir. ZEE/ZFI continuam dependentes de acto oficial estruturado e validado, regime e relações territoriais apropriadas; uma área sem confirmação não substitui essas evidências.
+- Proposta para as seis oportunidades de demonstração já públicas: preservar os registos e IDs, adicionar origem/tipo `demo` e identificá-los visivelmente como **Exemplo de demonstração**, separar a pesquisa de exemplos da pesquisa de oportunidades reais e excluir os exemplos dos resultados reais por defeito. Não os converter nem retirar em massa; cada registo real exigirá origem e proponente documentados. Nenhuma alteração foi feita nesta execução.
diff --git a/docs/investmoz/document-corpus-inventory-2026-10-06.csv b/docs/investmoz/document-corpus-inventory-2026-10-06.csv
new file mode 100644
index 00000000..628e79cc
--- /dev/null
+++ b/docs/investmoz/document-corpus-inventory-2026-10-06.csv
@@ -0,0 +1,78 @@
+"RecordType";"Path";"FileName";"SizeBytes";"SHA256";"TitleOrSubject";"DuplicateGroup";"Disposition";"Status";"Notes"
+"Anexo de instrução";"C:\Users\macbook\.codex/attachments/62a3324a-541d-414e-8fa8-8cc25c41ded3/Pasted text.txt";"Pasted text.txt";"8048";"7EE3460C0FC278057FFB283AB33D556F58ED8D9834F420C76103F69A0E18FBE3";"9. Prompt para gerar as migrations em Laravel";"";"Preservar como histórico de instrução; não importar como diploma/documento da biblioteca";"Acessível";"Contém referências fiscais em texto de instrução; não é, por si, fonte jurídica."
+"Anexo de instrução";"C:\Users\macbook\.codex/attachments/65a3763e-afd7-4ba0-b650-1f4c37f3cfbb/Pasted text.txt";"Pasted text.txt";"11460";"DFF87AD6BC8D49D487641E0DD577314BF78F75C3D0220A8802461C5DA0C48C7F";"Actua como arquitecto de software sénior e programador full stack especializado em:";"";"Preservar como histórico de instrução; não importar como diploma/documento da biblioteca";"Acessível";"Contém referências fiscais em texto de instrução; não é, por si, fonte jurídica."
+"Anexo de instrução";"C:\Users\macbook\.codex/attachments/1b36f6f0-93cf-49aa-9479-986a57bd18f4/Pasted text.txt";"Pasted text.txt";"37098";"470913BEA520A8EE267F8DF27C0849E12E0632D0EDE2D4556ED463C994B82161";"Pasted text sem título legível na primeira linha";"1b36f6f0-93cf-49aa-9479-986a57bd18f4 + 27922f5b-f041-47db-bbf1-6bc50f3a83dc";"Preservar como histórico de instrução; não importar como diploma/documento da biblioteca";"Acessível";"Contém referências fiscais em texto de instrução; não é, por si, fonte jurídica."
+"Anexo de instrução";"C:\Users\macbook\.codex/attachments/27922f5b-f041-47db-bbf1-6bc50f3a83dc/Pasted text.txt";"Pasted text.txt";"37098";"470913BEA520A8EE267F8DF27C0849E12E0632D0EDE2D4556ED463C994B82161";"Pasted text sem título legível na primeira linha";"1b36f6f0-93cf-49aa-9479-986a57bd18f4 + 27922f5b-f041-47db-bbf1-6bc50f3a83dc";"Preservar como histórico de instrução; não importar como diploma/documento da biblioteca";"Acessível";"Contém referências fiscais em texto de instrução; não é, por si, fonte jurídica."
+"Anexo de instrução";"C:\Users\macbook\.codex/attachments/602a2b85-7e0d-49a3-b048-de31d42fafc9/Pasted text.txt";"Pasted text.txt";"11118";"07BC673AA256F0CCC73138F70121077E948650683B2DD24F2EAD3DD75C76831B";"Actua como arquitecto sénior de inteligência artificial, engenheiro de machine learning e programador full stack especializado em:";"";"Preservar como histórico de instrução; não importar como diploma/documento da biblioteca";"Acessível";"Contém referências fiscais em texto de instrução; não é, por si, fonte jurídica."
+"Anexo de instrução";"C:\Users\macbook\.codex/attachments/a57fcca7-d9b3-45e9-96ab-575b1c5a3536/Pasted text.txt";"Pasted text.txt";"30350";"8E6DEAA7F0D738299B1AE3B8BF89B633641E0CC0CB5E7211461601C0C2B6D635";"Prompt Técnico — Unificação e Upgrade do Portal do Investidor";"";"Preservar como histórico de instrução; não importar como diploma/documento da biblioteca";"Acessível";"Contém referências fiscais em texto de instrução; não é, por si, fonte jurídica."
+"Anexo de instrução";"C:\Users\macbook\.codex/attachments/23b033e6-9ee3-4a6c-a41b-688fe7f09795/Pasted text.txt";"Pasted text.txt";"44742";"5DBF82C2D8BA2C9056B838C4565CB0DB9DE28DA4173075BFDC12B233B3C43FE9";"Implementação Integrada de ZEE, ZFI e Revisão do Catálogo de Investimentos";"";"Preservar como histórico de instrução; não importar como diploma/documento da biblioteca";"Acessível";"Contém referências fiscais em texto de instrução; não é, por si, fonte jurídica."
+"Anexo de instrução";"C:\Users\macbook\.codex/attachments/777f05aa-700e-4dd6-8dfe-e277214fb713/Pasted text.txt";"Pasted text.txt";"21531";"4A12DE1C0822EF8583CA5BC7F3D4FFA8DBBA1C44FBFBF4E0AADC345EFA696F65";"Pasted text sem título legível na primeira linha";"";"Preservar como histórico de instrução; não importar como diploma/documento da biblioteca";"Acessível";"Contém referências fiscais em texto de instrução; não é, por si, fonte jurídica."
+"Anexo de instrução";"C:\Users\macbook\.codex/attachments/5d9b9f23-04ef-4e14-89d5-bbe6abb4a06b/Pasted text.txt";"Pasted text.txt";"24256";"B3E17BB11D6EBAFA121B623C8FF8949812B35EF0CA5926A33C22F3E2093A01D9";"Prompt técnico de implementação — Mercado de Negócios Existentes do InvestMoz";"";"Preservar como histórico de instrução; não importar como diploma/documento da biblioteca";"Acessível";"Contém referências fiscais em texto de instrução; não é, por si, fonte jurídica."
+"Anexo de instrução";"C:\Users\macbook\.codex/attachments/7cb98fa0-99e1-4dcc-a3f1-30cfa71ea9ce/Pasted text.txt";"Pasted text.txt";"13591";"09933D0E63883E691A573EF23B7C13FA8FCF66556FE38618E222275F5E327C54";"InvestMoz — Modelo de Dados Geográficos, Plano de Implementação do MVP e APIs Territoriais";"";"Preservar como histórico de instrução; não importar como diploma/documento da biblioteca";"Acessível";"Contém referências fiscais em texto de instrução; não é, por si, fonte jurídica."
+"Anexo de instrução";"C:\Users\macbook\.codex/attachments/8a8b785e-862f-41bb-8d03-06fc6f1b853e/Pasted text.txt";"Pasted text.txt";"39222";"0755F56AE929FD37451D7DD589BED8E1F35C6655F2D674EC1BE788B2582CF31D";"InvestMoz — Modelo de Dados Geográficos, Plano de Implementação do MVP e APIs Territoriais";"";"Preservar como histórico de instrução; não importar como diploma/documento da biblioteca";"Acessível";"Contém referências fiscais em texto de instrução; não é, por si, fonte jurídica."
+"Anexo de instrução";"C:\Users\macbook\.codex/attachments/e11a00e4-8fcc-4ad8-8861-2fc0244c4324/Pasted text.txt";"Pasted text.txt";"23406";"C32107C1735D544D91C1C5E6F94C731E71871C66FF6DDCBECEDC4C86FD1630C7";"Prompt técnico de implementação — Mercado de Negócios Existentes do InvestMoz";"";"Preservar como histórico de instrução; não importar como diploma/documento da biblioteca";"Acessível";"Contém referências fiscais em texto de instrução; não é, por si, fonte jurídica."
+"Anexo de instrução";"C:\Users\macbook\.codex/attachments/e642d815-f449-495f-bd61-4b3e247e23de/Pasted text.txt";"Pasted text.txt";"9502";"5DFF89ADCE75A8B9E0A699BD31986A1A18FD3F25DE7218DA2800631365B4986E";"Prompt técnico completo — correcção definitiva da hierarquia e dos dropdowns de localização";"";"Preservar como histórico de instrução; não importar como diploma/documento da biblioteca";"Acessível";"Contém referências fiscais em texto de instrução; não é, por si, fonte jurídica."
+"Anexo de instrução";"C:\Users\macbook\.codex/attachments/9e57f88e-82d7-45a9-b1b3-4ae8cec7f713/Pasted text.txt";"Pasted text.txt";"11805";"F15768C79A22C039B9137B937DA003DD61912CD9E8BB9FD7A40C795DD0AF067E";"Prompt de Implementação da Cartografia";"";"Preservar como histórico de instrução; não importar como diploma/documento da biblioteca";"Acessível";"Contém referências fiscais em texto de instrução; não é, por si, fonte jurídica."
+"Anexo de instrução";"C:\Users\macbook\.codex/attachments/8865429e-0156-4167-b465-3595b0fda05f/Pasted text.txt";"Pasted text.txt";"5262310";"B2B4878730A3285D9FCACFF5236A15671802C172FF28C10704459AA2CFDFB9EB";"Pasted text sem título legível na primeira linha";"";"Preservar como histórico de instrução; não importar como diploma/documento da biblioteca";"Acessível";"Contém referências fiscais em texto de instrução; não é, por si, fonte jurídica."
+"Anexo de instrução";"C:\Users\macbook\.codex/attachments/bfb8dbf2-ecff-45be-8612-c1868ebe8343/Pasted text.txt";"Pasted text.txt";"6665";"CC874C1E0D7AB1D7779DF1664C524AE5F876EF7AF99B7FEC2358C4BE7D0E53C6";"Pasted text sem título legível na primeira linha";"";"Preservar como histórico de instrução; não importar como diploma/documento da biblioteca";"Acessível";"Contém referências fiscais em texto de instrução; não é, por si, fonte jurídica."
+"Anexo de instrução";"C:\Users\macbook\.codex/attachments/1dc38f3d-1455-4b18-8358-899a66386a7f/Pasted text.txt";"Pasted text.txt";"36260";"05EF8EC7DA01B185FF0872E917E9FA9C5C1E7407C3D547788F9AD53930DD5990";"Pasted text sem título legível na primeira linha";"";"Preservar como histórico de instrução; não importar como diploma/documento da biblioteca";"Acessível";"Contém referências fiscais em texto de instrução; não é, por si, fonte jurídica."
+"Anexo de instrução";"C:\Users\macbook\.codex/attachments/e51bbb68-0db4-41ba-84b3-e20754ba0d33/Pasted text.txt";"Pasted text.txt";"37962";"214044C4A4A71430AB8F271BA85256E48C6A08785DA0E48ECB7507A04F23B6D1";"Pasted text sem título legível na primeira linha";"";"Preservar como histórico de instrução; não importar como diploma/documento da biblioteca";"Acessível";"Contém referências fiscais em texto de instrução; não é, por si, fonte jurídica."
+"Anexo de instrução";"C:\Users\macbook\.codex/attachments/a2b1c919-0d1e-443b-bb5e-8917c877899d/Pasted text.txt";"Pasted text.txt";"54479";"06996499B4B5A6B84ABA61D43BCEBBE9BC7D710700BF279172DD92F1016C8BC6";"Mercado de Negócios Existentes do InvestMoz";"";"Preservar como histórico de instrução; não importar como diploma/documento da biblioteca";"Acessível";"Contém referências fiscais em texto de instrução; não é, por si, fonte jurídica."
+"Anexo de instrução";"C:\Users\macbook\.codex/attachments/b86ad3e6-37f4-4f04-9bbf-6dc33f69250e/Pasted text.txt";"Pasted text.txt";"6991";"A2AD18C729F00E37DB23C1B26615DCEE50960E0EED7346D6CC013E6A57C2C24D";"Pasted text sem título legível na primeira linha";"";"Preservar como histórico de instrução; não importar como diploma/documento da biblioteca";"Acessível";"Contém referências fiscais em texto de instrução; não é, por si, fonte jurídica."
+"Anexo de instrução";"C:\Users\macbook\.codex/attachments/890e42e3-ce0c-4cd0-903d-d97785be9ce5/Pasted text.txt";"Pasted text.txt";"68522";"D60559844425F03F4A722DFF3974BE91E8E4E279179BD58663AE164FCC9A68CC";"Mercado de Negócios Existentes do InvestMoz";"";"Preservar como histórico de instrução; não importar como diploma/documento da biblioteca";"Acessível";"Contém referências fiscais em texto de instrução; não é, por si, fonte jurídica."
+"Anexo de instrução";"C:\Users\macbook\.codex/attachments/5dbe0293-16ad-4975-8956-ea48778d6510/Pasted text.txt";"Pasted text.txt";"23804";"6E51CFCD4EE1329C51736E0FB1A47668A8E9550DE94EAD8FFC222BA22498179B";"Formulário de criação de oportunidade de negócio";"";"Preservar como histórico de instrução; não importar como diploma/documento da biblioteca";"Acessível";"Contém referências fiscais em texto de instrução; não é, por si, fonte jurídica."
+"Anexo de instrução";"C:\Users\macbook\.codex/attachments/2daec0e8-1c8a-4606-bb4a-b7fccd0ea7bf/Pasted text.txt";"Pasted text.txt";"0";"E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855";;"";"Preservar como histórico de instrução; não importar como diploma/documento da biblioteca";"Acessível";"Texto de prompt/instrução, não binário documental."
+"Anexo de instrução";"C:\Users\macbook\.codex/attachments/8528a9d2-b934-4bb3-a132-a18b7e13414d/Pasted text.txt";"Pasted text.txt";"30136";"6C2AE3E9B43133940DD90FA2BE0D24C07EED06ACE5799CEA3EFC25574C0A0E8E";"Script PostgreSQL/PostGIS das tabelas a implementar";"";"Preservar como histórico de instrução; não importar como diploma/documento da biblioteca";"Acessível";"Contém referências fiscais em texto de instrução; não é, por si, fonte jurídica."
+"Anexo de instrução";"C:\Users\macbook\.codex/attachments/bf1d310a-d3a8-48fc-953f-0648622a6c85/Pasted text.txt";"Pasted text.txt";"81460";"FD7878021AE5D18C9D4A9A79073552C36876D59726B8FB2612752F2594080CA0";"PROMPT MESTRE ";"";"Preservar como histórico de instrução; não importar como diploma/documento da biblioteca";"Acessível";"Contém referências fiscais em texto de instrução; não é, por si, fonte jurídica."
+"Anexo de instrução";"C:\Users\macbook\.codex/attachments/ce3b9157-6463-4c3f-af50-6d53cba78a23/Pasted text.txt";"Pasted text.txt";"30586";"6381A59FBE8BD8EE5A76D8755CEB320EF2D14FC1E628B8E1CBEF2C5EB83E30C8";"Instrução principal para o Codex";"";"Preservar como histórico de instrução; não importar como diploma/documento da biblioteca";"Acessível";"Contém referências fiscais em texto de instrução; não é, por si, fonte jurídica."
+"Anexo de instrução";"C:\Users\macbook\.codex/attachments/7235d6fa-a6a3-453f-a97c-b6741c083d10/Pasted text.txt";"Pasted text.txt";"16673";"2626DF4CA70D52CB5F90247AF7B6EEBBB251FC64B4915EE0ED1FBBB693CB3586";"Prompt de Validação da Actividade Realizada até ao Momento";"";"Preservar como histórico de instrução; não importar como diploma/documento da biblioteca";"Acessível";"Contém referências fiscais em texto de instrução; não é, por si, fonte jurídica."
+"Anexo de instrução";"C:\Users\macbook\.codex/attachments/22ddc8a3-c393-40ff-9960-dcf7bd9e34f6/Pasted text.txt";"Pasted text.txt";"8230";"5464CDAAB509637A44ADC08FE621FDE45A0BC16C8FC3AA9483FB1B9F0F9E0E85";"Prompt para o Codex — Execução do Plano de Remediação e Validação por Evidências";"";"Preservar como histórico de instrução; não importar como diploma/documento da biblioteca";"Acessível";"Contém referências fiscais em texto de instrução; não é, por si, fonte jurídica."
+"Anexo de instrução";"C:\Users\macbook\.codex/attachments/c1ac842e-1b98-4f18-82bf-5155dbddf288/Pasted text.txt";"Pasted text.txt";"11603";"BD55CAC92926FD804E358921924740EB9C7E649AF7A845FE85D2C3686AE2EF09";"Pasted text sem título legível na primeira linha";"";"Preservar como histórico de instrução; não importar como diploma/documento da biblioteca";"Acessível";"Texto de prompt/instrução, não binário documental."
+"Anexo de instrução";"C:\Users\macbook\.codex/attachments/d5886992-638e-479f-bc30-e327bd7a10ee/Pasted text.txt";"Pasted text.txt";"12246";"F0DA2B58AF2E30CC1B1CC353F3A6B00A592C800E17B8D00BD2C13DCBF16C5A78";"Estrutura proposta.";"";"Preservar como histórico de instrução; não importar como diploma/documento da biblioteca";"Acessível";"Contém referências fiscais em texto de instrução; não é, por si, fonte jurídica."
+"Anexo de instrução";"C:\Users\macbook\.codex/attachments/a1795b4c-c360-4e31-8eef-2daeb930721c/Pasted text.txt";"Pasted text.txt";"23694";"24F36A01C9B6D0894155CBF09C17F128904626935461897BF59CE2B811D08D9D";"Prompt completo para Codex/Copilot — React + Laravel + PostgreSQL/PostGIS: ";"";"Preservar como histórico de instrução; não importar como diploma/documento da biblioteca";"Acessível";"Contém referências fiscais em texto de instrução; não é, por si, fonte jurídica."
+"Anexo de instrução";"C:\Users\macbook\.codex/attachments/cfe4012e-cb54-4a42-8fcf-9567bea45d7d/Pasted text.txt";"Pasted text.txt";"34983";"8AB2B0DF6B41A483D72AA852984C627A1C41DEA3F05E0F13AC3EE0AD6D680B65";"Etapa 9 — Formulário React com dropdowns";"";"Preservar como histórico de instrução; não importar como diploma/documento da biblioteca";"Acessível";"Contém referências fiscais em texto de instrução; não é, por si, fonte jurídica."
+"Anexo de instrução";"C:\Users\macbook\.codex/attachments/50aa6a01-de6b-4fff-9625-46dbbebd033c/Pasted text.txt";"Pasted text.txt";"13287";"129DF97B64E3747D626B89E25F70BBA14AD50CCDB47F08EEEDEDD41D0509EBE0";"19.1 Prompt final para Codex";"";"Preservar como histórico de instrução; não importar como diploma/documento da biblioteca";"Acessível";"Contém referências fiscais em texto de instrução; não é, por si, fonte jurídica."
+"Anexo de instrução";"C:\Users\macbook\.codex/attachments/2368cafa-157f-4177-ac75-255563247749/Pasted text.txt";"Pasted text.txt";"16046";"300EDE994BA6CBACDF7FFEBB4CBD75777ECCB3F359842491404357E8A549B235";"Etapa 10 — Motor fiscal principal";"";"Preservar como histórico de instrução; não importar como diploma/documento da biblioteca";"Acessível";"Contém referências fiscais em texto de instrução; não é, por si, fonte jurídica."
+"Anexo de instrução";"C:\Users\macbook\.codex/attachments/ce71e769-044b-4eaa-9f8a-793e52cd644d/Pasted text.txt";"Pasted text.txt";"6128";"0A64D4FE9DC2D80211568D74CC49335472D7A2BE01A199223BF4B1A0FB58E983";"ID Regra/cenário e entradas Resultado esperado Prioridade";"";"Preservar como histórico de instrução; não importar como diploma/documento da biblioteca";"Acessível";"Contém referências fiscais em texto de instrução; não é, por si, fonte jurídica."
+"Anexo de instrução";"C:\Users\macbook\.codex/attachments/77627963-3e1c-4d41-add5-18eedbf62b01/Pasted text.txt";"Pasted text.txt";"16486";"62719366D33C647930CC4BC92A07D8E933C4F0E0DC06D91E2464DA86CA4B6F8A";"# Evolução sequencial do simulador multiuso da investeMoz";"";"Preservar como histórico de instrução; não importar como diploma/documento da biblioteca";"Acessível";"Contém referências fiscais em texto de instrução; não é, por si, fonte jurídica."
+"Anexo de instrução";"C:\Users\macbook\.codex/attachments/ddd6740c-486e-4ebf-a11e-c9687206178a/Pasted text.txt";"Pasted text.txt";"7103";"149673C0E35573716086D72F65E1B8E6C805C4F890B44722B839D4CE7E6CAF15";"Formulário de cadastro de oportunidades de negócio da InvestMoz";"";"Preservar como histórico de instrução; não importar como diploma/documento da biblioteca";"Acessível";"Contém referências fiscais em texto de instrução; não é, por si, fonte jurídica."
+"Anexo de instrução";"C:\Users\macbook\.codex/attachments/13ef8ec2-be07-4b10-93a1-9fe134b928fa/Pasted text.txt";"Pasted text.txt";"14654";"8185BED1E45611AF8861C8F00568A7DA6901896E2650355C41642805485FFAE1";"Prompt master para revisão, correcção e implementação consolidada do InvestMoz";"";"Preservar como histórico de instrução; não importar como diploma/documento da biblioteca";"Acessível";"Contém referências fiscais em texto de instrução; não é, por si, fonte jurídica."
+"Anexo de instrução";"C:\Users\macbook\.codex/attachments/acb359eb-1a65-486b-8680-c6abeacccef3/Pasted text.txt";"Pasted text.txt";"14837";"99E36A08038F2540114AD87C32423CE9F0AA6C7C7AC866372273B4FD33DF3765";"Prompt consolidado para o InvestMoz — Terra DUAT formulários e acompanhamento";"";"Preservar como histórico de instrução; não importar como diploma/documento da biblioteca";"Acessível";"Contém referências fiscais em texto de instrução; não é, por si, fonte jurídica."
+"Anexo de instrução";"C:\Users\macbook\.codex/attachments/95cf3a5e-fdbf-4005-a877-57d4ac1f93c5/Pasted text.txt";"Pasted text.txt";"11261";"A2434658AD9E75E59F897CB57C4C2934B8A28F879E32584F3D00064900C6A5EE";"# Prompt master consolidado — Conclusão e melhoria do portal InvestMoz";"";"Preservar como histórico de instrução; não importar como diploma/documento da biblioteca";"Acessível";"Contém referências fiscais em texto de instrução; não é, por si, fonte jurídica."
+"Anexo de instrução";"C:\Users\macbook\.codex/attachments/c6245408-7052-4478-8804-377b282e8203/Pasted text.txt";"Pasted text.txt";"30976";"600240DD64B59B82BDB92E471B475F9EFC96EAB67F9CD532A432E1E7A324F0C9";"Benchmark de melhorias do InvesteMoz";"";"Preservar como histórico de instrução; não importar como diploma/documento da biblioteca";"Acessível";"Contém referências fiscais em texto de instrução; não é, por si, fonte jurídica."
+"Anexo de instrução";"C:\Users\macbook\.codex/attachments/a89743e5-3439-4543-9382-2df32e6ceaa9/Pasted text.txt";"Pasted text.txt";"6636";"574657F7FC612BF7760A31F200978B289A911DF39964B8C60E3B357845D8BD4D";"Correcção e integração funcional do portal InvesteMoz";"";"Preservar como histórico de instrução; não importar como diploma/documento da biblioteca";"Acessível";"Contém referências fiscais em texto de instrução; não é, por si, fonte jurídica."
+"Anexo de instrução";"C:\Users\macbook\.codex/attachments/dd19a693-8805-4f81-a65c-bf2af031a37a/Pasted text.txt";"Pasted text.txt";"9346";"2453530CC98FFD9B270EEF47786A9D596701E4831E2A3C16C3765152CC916494";"Integração de ZEE, ZFI e parques industriais no InvesteMoz";"";"Preservar como histórico de instrução; não importar como diploma/documento da biblioteca";"Acessível";"Contém referências fiscais em texto de instrução; não é, por si, fonte jurídica."
+"Anexo de instrução";"C:\Users\macbook\.codex/attachments/d53670c4-f46a-4d49-b188-770b86bdda19/Pasted text.txt";"Pasted text.txt";"8127";"C71EC7ADB5AF079FC5C93536107FE30FFC0906700B5EE5EEF6BD2CCA1665DFDE";"Integração dos parques industriais e ZFI no InvesteMoz";"";"Preservar como histórico de instrução; não importar como diploma/documento da biblioteca";"Acessível";"Contém referências fiscais em texto de instrução; não é, por si, fonte jurídica."
+"Anexo de instrução";"C:\Users\macbook\.codex/attachments/aa68e897-14b6-4504-ad05-93c2e3f0b4fc/Pasted text.txt";"Pasted text.txt";"9536";"B207D14E53CF13E621A01D0E2BF4BB82BDB65AD0BA07C457B42923299D0336A9";"Parques Industriais e Zonas Francas Industriais em Moçambique";"";"Preservar como histórico de instrução; não importar como diploma/documento da biblioteca";"Acessível";"Contém referências fiscais em texto de instrução; não é, por si, fonte jurídica."
+"Anexo de instrução";"C:\Users\macbook\.codex/attachments/2fabbcdf-a098-4a0c-907f-b5cc428c4608/Pasted text.txt";"Pasted text.txt";"12095";"991D4B34E6DADD23F4C66A61DE509FBAC1ABE83D8CBB316DD24BA400C6F13451";"Melhoria integrada do simulador InvesteMoz";"";"Preservar como histórico de instrução; não importar como diploma/documento da biblioteca";"Acessível";"Contém referências fiscais em texto de instrução; não é, por si, fonte jurídica."
+"Anexo de instrução";"C:\Users\macbook\.codex/attachments/e1fa9a32-14ae-46f3-ac02-c7115c832175/Pasted text.txt";"Pasted text.txt";"8246";"55050F60A15D18B119C83799231A0AF34B00738F7AE8202A232A07DF23F65FD1";"Biblioteca integrada de Documentos e Formulários do InvesteMoz";"";"Preservar como histórico de instrução; não importar como diploma/documento da biblioteca";"Acessível";"Contém referências fiscais em texto de instrução; não é, por si, fonte jurídica."
+"Anexo de instrução";"C:\Users\macbook\.codex/attachments/11c236f7-e954-499a-b290-3d6dce07543a/Pasted text.txt";"Pasted text.txt";"5158";"A8241744DB40871B36D0A2B39796B7168FADCF243E02A352FA794176DA389C95";"Pasted text sem título legível na primeira linha";"11c236f7-e954-499a-b290-3d6dce07543a + f003a1f1-4ca9-4193-9790-0ba7c1db9812";"Preservar como histórico de instrução; não importar como diploma/documento da biblioteca";"Acessível";"Contém referências fiscais em texto de instrução; não é, por si, fonte jurídica."
+"Anexo de instrução";"C:\Users\macbook\.codex/attachments/f003a1f1-4ca9-4193-9790-0ba7c1db9812/Pasted text.txt";"Pasted text.txt";"5158";"A8241744DB40871B36D0A2B39796B7168FADCF243E02A352FA794176DA389C95";"Pasted text sem título legível na primeira linha";"11c236f7-e954-499a-b290-3d6dce07543a + f003a1f1-4ca9-4193-9790-0ba7c1db9812";"Preservar como histórico de instrução; não importar como diploma/documento da biblioteca";"Acessível";"Contém referências fiscais em texto de instrução; não é, por si, fonte jurídica."
+"Anexo de instrução";"C:\Users\macbook\.codex/attachments/4b4f7002-adf3-4004-abb0-f389778dc08b/Pasted text.txt";"Pasted text.txt";"5601";"02F44BAFEA4BA47ADC8B24284D4B189AB9F1DB3F6C1D1D63FC06178C400F9EE0";"Migração e associação das fontes legais do InvesteMoz";"";"Preservar como histórico de instrução; não importar como diploma/documento da biblioteca";"Acessível";"Contém referências fiscais em texto de instrução; não é, por si, fonte jurídica."
+"Anexo de instrução";"C:\Users\macbook\.codex/attachments/b93f36c6-ac2c-45f4-995d-577c09b3862e/Pasted text.txt";"Pasted text.txt";"10274";"9F6DD1D9C0CA8628443EDC69A6151BA49F96A8E044AC8AFACE249C4957DAFB86";"Conclusão da biblioteca Documentos e Formulários do InvesteMoz";"";"Preservar como histórico de instrução; não importar como diploma/documento da biblioteca";"Acessível";"Contém referências fiscais em texto de instrução; não é, por si, fonte jurídica."
+"Anexo de instrução";"C:\Users\macbook\.codex/attachments/006f3ea6-5bf4-4315-b3b6-5dda9c3a6566/Pasted text.txt";"Pasted text.txt";"6842";"DBF5AFDCA293302BB84389AC33AA576C25859F8FA54E26D0B9687B9C564BE005";"Recuperação funcional do InvesteMoz";"";"Preservar como histórico de instrução; não importar como diploma/documento da biblioteca";"Acessível";"Contém referências fiscais em texto de instrução; não é, por si, fonte jurídica."
+"Fonte fiscal arquivada";"C:\laragon\www\portal-investidor\docs\investmoz\simulations\source-archive\BR-248-I-Suplemento-1-2025.pdf";"BR-248-I-Suplemento-1-2025.pdf";"440018";"CCF2A8DEECA820F2DD076B4B2141B1EFE3F6A806568C3A587CA59A8E92F4C251";"BR-248-I-Suplemento-1-2025";"";"Importado para documents/portal_resources; consulta pública pendente de acção de gestão";"Acessível / importado / privado";"SHA-256 registado e submetido a ClamAV; hash analisado coincide."
+"Fonte fiscal arquivada";"C:\laragon\www\portal-investidor\docs\investmoz\simulations\source-archive\BR-89-I-Serie-2017-06-08.pdf";"BR-89-I-Serie-2017-06-08.pdf";"9456358";"11F0CAB15EB6E51010952203FFA24FDC533A24FD642E19525FE4F46B17A1F5E5";"BR-89-I-Serie-2017-06-08";"";"Importado para documents/portal_resources; consulta pública pendente de acção de gestão";"Acessível / importado / privado";"SHA-256 registado e submetido a ClamAV; hash analisado coincide."
+"Fonte fiscal arquivada";"C:\laragon\www\portal-investidor\docs\investmoz\simulations\source-archive\Lei-22-2022-IVA-republicada-BR-250-2-suplemento.pdf";"Lei-22-2022-IVA-republicada-BR-250-2-suplemento.pdf";"1834284";"EA1B0DC6F5CF6E595F6D9A19E7042F398999E0087CAEF1785F6E8FAC571B5F53";"Lei-22-2022-IVA-republicada-BR-250-2-suplemento";"";"Importado para documents/portal_resources; consulta pública pendente de acção de gestão";"Acessível / importado / privado";"SHA-256 registado e submetido a ClamAV; hash analisado coincide."
+"Fonte de referência territorial";"C:\laragon\www\portal-investidor\storage\app\private\geo-ine-source-20260928\original.pdf";"INE-Codificador-Divisao-Politico-Administrativa-2012.pdf";"67067";"5C69B6F189A1D3F5307E0CF7DD8224554705158A682637D3105AFAD615DB26E2";"INE Codificador DPA edição 07-06-2012";"";"Importado para documents/portal_resources; consulta pública pendente de acção de gestão";"Acessível / importado / privado";"Fonte oficial identificada e hash coincidente; actualidade territorial e direitos do artefacto por rever."
+"Fonte indicada, não acessível";"Legislacao/Lei 13_2016_Reimpressao Codigo do IVA.pdf";"Lei 13_2016_Reimpressao Codigo do IVA.pdf";"";"";"Lei 13/2016, republicação integral do CIVA";"";"Pendente de localização/recepção; não criar ficheiro fictício";"Inacessível nesta execução";"Texto-base importante para consolidação; ficheiro não encontrado nos caminhos disponíveis; OCR anterior inconclusivo."
+"Fonte indicada, não acessível";"Legislacao/RectificaçãoLei.13.2016_Republicação.Codigo.IVA.pdf";"RectificaçãoLei.13.2016_Republicação.Codigo.IVA.pdf";"";"";"CIVA rectificado / republicação, 15 páginas referidas em inventário anterior";"";"Pendente de localização/recepção; não criar ficheiro fictício";"Inacessível nesta execução";"Versão distinta; não confundir com o aviso BR 89. Ficheiro não localizado neste workspace/perfil."
+"Fonte indicada, não acessível";"Legislacao/alteracao CIVA 2023.pdf";"alteracao CIVA 2023.pdf";"";"";"Alteração CIVA 2023";"";"Pendente de localização/recepção; não criar ficheiro fictício";"Inacessível nesta execução";"Ficheiro referido por inventário anterior, não localizado nesta execução; conteúdo/artigos ainda não reconciliados."
+"Fonte indicada, não acessível";"Legislacao/Legislacao/Legislacao/Lei 22 2022 IVA.pdf";"Lei 22 2022 IVA.pdf";"";"";"Lei 22/2022 IVA, cópia de 1.989.462 bytes referida antes";"";"Pendente de localização/recepção; não criar ficheiro fictício";"Inacessível nesta execução";"Versão/cópia diferente do exemplar AT arquivado (1.834.284 bytes); não deduplicar sem inspeccionar edição e hash; original não localizado."
+"Fonte indicada, não acessível";"Legislacao/Lei 12.2025_IRPC.pdf";"Lei 12.2025_IRPC.pdf";"";"";"Lei 12/2025 IRPC, cópia de 511.828 bytes referida antes";"";"Pendente de localização/recepção; não criar ficheiro fictício";"Inacessível nesta execução";"Pode sobrepor-se parcialmente ao BR 248; preservar como versão candidata até comparação de identidade e hash; ficheiro não localizado."
+"Fonte indicada, não acessível";"Legislacao/Legislacao/2009/Lei 4_2009 Código dos Benefícios Fiscais - versão consolidada.pdf";"Lei 4_2009 Código dos Benefícios Fiscais - versão consolidada.pdf";"";"";"Lei 4/2009, Código dos Benefícios Fiscais";"";"Pendente de localização/recepção; não criar ficheiro fictício";"Inacessível nesta execução";"Extração anterior parcial; ficheiro não localizado. Não associar automaticamente a incentivo executável."
+"Fonte indicada, não acessível";"Legislacao/Legislacao/a digitalizar/ISPC_Financas.pdf";"ISPC_Financas.pdf";"";"";"ISPC_Financas.pdf";"";"Pendente de localização/recepção; não criar ficheiro fictício";"Inacessível nesta execução";"Scan/camada de texto insuficiente segundo inventário anterior; ficheiro não localizado nesta execução."
+"Fonte indicada, não acessível";"Informacao Publica/Legislacao - resumos/Reforma fiscal de 2026.docx";"Reforma fiscal de 2026.docx";"";"";"Reforma fiscal de 2026 (resumo secundário)";"";"Pendente de localização/recepção; não criar ficheiro fictício";"Inacessível nesta execução";"Já foi lido e reconciliado como fonte secundária em relatório anterior; binário não está nos caminhos acessíveis agora; não é autoridade primária."
+"Fonte indicada, não acessível";"Caminho não registado no dossier actual / CSV de revisão pessoal";" CSV de revisão pessoal";"";"";"CSV de revisão pessoal ligado à revisão fiscal";"";"Pendente de localização/recepção; não criar ficheiro fictício";"Inacessível nesta execução";"Nome/caminho/hash não constam do registo acessível; não localizado. Precisa caminho de origem para inventário por ficheiro."
+"Fonte indicada, não acessível";"Caminho não registado / Modelo 20 OCAM";" Modelo 20 OCAM";"";"";"Declaração anual de informação contabilística e fiscal, exercício 2025";"";"Pendente de localização/recepção; não criar ficheiro fictício";"Inacessível nesta execução";"Referida como documento empresarial com dados fiscais privados; fora da biblioteca pública. Ficheiro não acessível neste workspace; manter fora do acervo público."
+"Cópia privada ou derivado";"C:\laragon\www\portal-investidor\storage\app\private\geo-ine-review-20260928\original.pdf";"original.pdf";"67067";"5C69B6F189A1D3F5307E0CF7DD8224554705158A682637D3105AFAD615DB26E2";"original";"C:\laragon\www\portal-investidor\storage\app\private\geo-ine-review-20260928\original.pdf + C:\laragon\www\portal-investidor\storage\app\private\geo-ine-review-20260928-v2\original.pdf + C:\laragon\www\portal-investidor\storage\app\private\portal-documents\b0ea969a-30ec-477b-8b8b-b7af82a11bfe";"Duplicate private copy of INE source; reconcile by SHA-256";"Acessível em armazenamento privado";"Inventariado por caminho, tamanho e SHA-256; não publicado nem aberto nesta revisão."
+"Cópia privada ou derivado";"C:\laragon\www\portal-investidor\storage\app\private\geo-ine-review-20260928-v2\layout.txt";"layout.txt";"57268";"783A7886ED5A27B8F9D0063A787928E9A4B09723C28A260C019C2DE215A670A3";"layout";"";"Derived extraction/layout; not legal source";"Acessível em armazenamento privado";"Inventariado por caminho, tamanho e SHA-256; não publicado nem aberto nesta revisão."
+"Cópia privada ou derivado";"C:\laragon\www\portal-investidor\storage\app\private\geo-ine-review-20260928-v2\original.pdf";"original.pdf";"67067";"5C69B6F189A1D3F5307E0CF7DD8224554705158A682637D3105AFAD615DB26E2";"original";"C:\laragon\www\portal-investidor\storage\app\private\geo-ine-review-20260928\original.pdf + C:\laragon\www\portal-investidor\storage\app\private\geo-ine-review-20260928-v2\original.pdf + C:\laragon\www\portal-investidor\storage\app\private\portal-documents\b0ea969a-30ec-477b-8b8b-b7af82a11bfe";"Duplicate private copy of INE source; reconcile by SHA-256";"Acessível em armazenamento privado";"Inventariado por caminho, tamanho e SHA-256; não publicado nem aberto nesta revisão."
+"Cópia privada ou derivado";"C:\laragon\www\portal-investidor\storage\app\private\geo-ine-review-20260928-v2\owner-acceptance-242d74394061d175e70e56161a48f66f02cce8d5989fc61098599d082da38f60.json";"owner-acceptance-242d74394061d175e70e56161a48f66f02cce8d5989fc61098599d082da38f60.json";"808";"242D74394061D175E70E56161A48F66F02CCE8D5989FC61098599D082DA38F60";"Artefacto de revisão/proveniência; conteúdo privado não reproduzido";"";"Private review/provenance artefact; do not publish";"Acessível em armazenamento privado";"Inventariado por caminho, tamanho e SHA-256; não publicado nem aberto nesta revisão."
+"Cópia privada ou derivado";"C:\laragon\www\portal-investidor\storage\app\private\geo-ine-review-20260928-v2\report.json";"report.json";"1115";"F455D6D995E7A970F9F7DC0CAAC58DEE46E5C5D60031D143CADB840741341BCA";"Artefacto de revisão/proveniência; conteúdo privado não reproduzido";"";"Private review/provenance artefact; do not publish";"Acessível em armazenamento privado";"Inventariado por caminho, tamanho e SHA-256; não publicado nem aberto nesta revisão."
+"Cópia privada ou derivado";"C:\laragon\www\portal-investidor\storage\app\private\geo-ine-review-20260928-v2\review.csv";"review.csv";"82109";"3F80D270B67CCBB3D8A2900A8CC847AC2BFEDBA4A9A2867C47FD5DBF75DD6A70";"Artefacto de revisão/proveniência; conteúdo privado não reproduzido";"";"Private review/provenance artefact; do not publish";"Acessível em armazenamento privado";"Inventariado por caminho, tamanho e SHA-256; não publicado nem aberto nesta revisão."
+"Cópia privada ou derivado";"C:\laragon\www\portal-investidor\storage\app\private\geo-ine-source-20260928\layout.txt";"layout.txt";"57191";"2FB8EBE44BB0F9E22E382436890A26822592E1DA023B56CA392E9AAFDDE7D85A";"layout";"";"Derived extraction/layout; not legal source";"Acessível em armazenamento privado";"Inventariado por caminho, tamanho e SHA-256; não publicado nem aberto nesta revisão."
+"Cópia privada ou derivado";"C:\laragon\www\portal-investidor\storage\app\private\portal-documents\58abf343-e1f5-446d-a9cb-3255f376ece9";"58abf343-e1f5-446d-a9cb-3255f376ece9";"9456358";"11F0CAB15EB6E51010952203FFA24FDC533A24FD642E19525FE4F46B17A1F5E5";"58abf343-e1f5-446d-a9cb-3255f376ece9";"";"Private stored copy already represented by its document record";"Acessível em armazenamento privado";"Inventariado por caminho, tamanho e SHA-256; não publicado nem aberto nesta revisão."
+"Cópia privada ou derivado";"C:\laragon\www\portal-investidor\storage\app\private\portal-documents\5c860c48-e8cb-4d8d-ab9b-2e3ea7036ce2";"5c860c48-e8cb-4d8d-ab9b-2e3ea7036ce2";"1834284";"EA1B0DC6F5CF6E595F6D9A19E7042F398999E0087CAEF1785F6E8FAC571B5F53";"5c860c48-e8cb-4d8d-ab9b-2e3ea7036ce2";"";"Private stored copy already represented by its document record";"Acessível em armazenamento privado";"Inventariado por caminho, tamanho e SHA-256; não publicado nem aberto nesta revisão."
+"Cópia privada ou derivado";"C:\laragon\www\portal-investidor\storage\app\private\portal-documents\64c46765-96b9-4f43-afc6-f5b730c010c4";"64c46765-96b9-4f43-afc6-f5b730c010c4";"440018";"CCF2A8DEECA820F2DD076B4B2141B1EFE3F6A806568C3A587CA59A8E92F4C251";"64c46765-96b9-4f43-afc6-f5b730c010c4";"";"Private stored copy already represented by its document record";"Acessível em armazenamento privado";"Inventariado por caminho, tamanho e SHA-256; não publicado nem aberto nesta revisão."
+"Cópia privada ou derivado";"C:\laragon\www\portal-investidor\storage\app\private\portal-documents\b0ea969a-30ec-477b-8b8b-b7af82a11bfe";"b0ea969a-30ec-477b-8b8b-b7af82a11bfe";"67067";"5C69B6F189A1D3F5307E0CF7DD8224554705158A682637D3105AFAD615DB26E2";"b0ea969a-30ec-477b-8b8b-b7af82a11bfe";"C:\laragon\www\portal-investidor\storage\app\private\geo-ine-review-20260928\original.pdf + C:\laragon\www\portal-investidor\storage\app\private\geo-ine-review-20260928-v2\original.pdf + C:\laragon\www\portal-investidor\storage\app\private\portal-documents\b0ea969a-30ec-477b-8b8b-b7af82a11bfe";"Private stored copy already represented by its document record";"Acessível em armazenamento privado";"Inventariado por caminho, tamanho e SHA-256; não publicado nem aberto nesta revisão."
diff --git a/docs/investmoz/document-corpus-reconciliation-2026-10-06.md b/docs/investmoz/document-corpus-reconciliation-2026-10-06.md
new file mode 100644
index 00000000..a2021423
--- /dev/null
+++ b/docs/investmoz/document-corpus-reconciliation-2026-10-06.md
@@ -0,0 +1,97 @@
+# Reconciliação do acervo documental e fontes fiscais
+
+Data: 06-10-2026. Inventário limitado aos anexos enumerados no manifesto local do Codex, aos arquivos documentais do repositório, às referências de fontes fiscais já registadas e ao armazenamento privado que serve esses registos. O índice completo por caminho, tamanho, SHA-256, destino e estado encontra-se em [document-corpus-inventory-2026-10-06.csv](document-corpus-inventory-2026-10-06.csv).
+
+## Reconciliação quantitativa
+
+| Medida | Resultado desta revisão |
+|---|---:|
+| Anexos enumerados no manifesto | 52 ficheiros `.txt` |
+| Conteúdos de anexo únicos por SHA-256 | 50 |
+| Pares de anexos exactamente duplicados | 2 pares: `1b36f6f0…` / `27922f5b…` e `11c236f7…` / `f003a1f1…` |
+| Caminhos de ficheiros acessíveis inventariados | 67 |
+| Conteúdos únicos por SHA-256 entre esses caminhos | 59 |
+| Documentos-fonte substantivos únicos acessíveis | 4 |
+| Documentos binários de fonte actualmente acessíveis | 4 documentos distintos: Lei 22/2022, BR 248/2025, BR 89/2017 e Codificador DPA do INE |
+| Cópias/derivados privados adicionais inventariados | 11 objectos, incluindo cópias dos quatro PDFs, extracções e artefactos internos de revisão |
+| Registos documentais importados na base activa | 4 documentos e 4 recursos; todos com scan limpo e hash do scan igual ao hash armazenado |
+| Publicados para consulta pública | 0 |
+| Privados na biblioteca activa | 4 |
+| Fontes anteriormente referidas mas inacessíveis nesta execução | 10 entradas, cada uma listada no CSV com caminho/nome tal como registado e pendência concreta |
+| Registos no CSV de inventário | 77 objectos/entradas; inclui anexos, originais, cópias, derivados e fontes inacessíveis |
+
+Os 52 anexos são instruções, prompts, texto de terminal/SQL e outros conteúdos colados; não são 52 diplomas. Foram preservados como histórico, fora da biblioteca pública. Dois pares têm bytes idênticos; as restantes cópias mantêm-se individualizadas. Os 59 hashes únicos incluem 50 textos de instrução, quatro fontes substantivas e cinco conteúdos auxiliares únicos; cópias privadas não contam como documentos novos. O CSV não copia o conteúdo potencialmente privado dos anexos ou dos artefactos de revisão.
+
+Os 10 itens inacessíveis são as sete fontes antigas referidas por caminho relativo `Legislacao/...` (CIVA republicado de 2016; cópia do CIVA rectificado; alteração CIVA 2023; outra cópia de Lei 22/2022; Lei 12/2025; Código dos Benefícios Fiscais Lei 4/2009; `ISPC_Financas.pdf`), o resumo `Informacao Publica/Legislacao - resumos/Reforma fiscal de 2026.docx`, o CSV de revisão pessoal cujo nome/caminho não ficou registado e o Modelo 20 OCAM. Pesquisas nos directórios Documents, Downloads, Desktop e OneDrive pelos nomes conhecidos não localizaram os binários. O Modelo 20 é tratado como informação empresarial confidencial, não como documento para publicação. Para os restantes, o destino é pendente de localização/recepção, sem fabricar substitutos. Os nomes e caminhos de origem disponíveis estão individualizados no CSV.
+
+## Lote documental e estados independentes
+
+O lote real de quatro fontes foi previamente importado para a biblioteca activa, com cópia privada, hash e análise antimalware. A pré-visualização repetida de `php artisan documents:import-legal-source-archive` nesta execução encontrou os quatro hashes já registados e devolveu `reused/reconciled` para cada um; terminou com “Preview only; no files or database rows were changed.” Não criou cópias nem alterou a base activa.
+
+| Recurso | Disponibilidade | Situação documental | Uso fiscal |
+|---:|---|---|---|
+| 1 — Lei n.º 22/2022, CIVA republicado | Privado; consulta pública pendente da acção autenticada de disponibilização | Fonte oficial e bytes confirmados; diploma alterador; edição/data visíveis; não equivale a consolidação integral actual | Art. 17(1) citado como evidência documental; não activa taxa nem cálculo |
+| 2 — BR n.º 248/2025, Suplemento n.º 1 | Privado; consulta pública pendente da mesma acção autenticada | Edição oficial confirmada; contém seis diplomas distintos; vigência deve ser tratada por diploma/dispositivo | Citações ligadas às chaves técnicas como evidência; sem activação de regras |
+| 3 — BR n.º 89/2017, aviso de rectificação | Privado; autorização anterior permite disponibilizar como cópia fornecida | A ficha INM identifica o aviso, mas os bytes/anexos da cópia não foram comparados com exemplar oficial; autenticidade da cópia não confirmada | Associação ao CIVA fica pendente; não é fundamento confirmado |
+| 4 — Codificador DPA INE, edição 07-06-2012 | Privado; consulta pública pendente da mesma acção autenticada | PDF comparado com exemplar servido pelo INE; actualidade territorial e direitos do artefacto PDF não determinam a autenticidade da edição | Sem uso como fonte fiscal; não usar automaticamente como cadastro territorial actual |
+
+A autorização anterior está registada com proveniência `user-instruction`, sem atribuição a gestor fictício, e abrange os recursos 1–4. Não exige nova aprovação genérica. O mecanismo de publicação permanece protegido por autenticação, email verificado e permissão de gestão. A acção preparada está em `/management/portal-services`, botão **“Aplicar autorização e disponibilizar documentos elegíveis”**; cada item é verificado individualmente, o executor autenticado fica registado e nenhuma regra fiscal é alterada. A consulta pública só estará concluída depois dessa acção ser executada e confirmada em `/documentos`.
+
+Na base activa, os recursos 1–3 têm apenas relações gerais `module=simulator` e/ou `theme=*`, todas com revisão `pending`; o recurso 4 não tem relação de módulo/requisito. Isso não bloqueia cadastro nem consulta. A migração de associações `requirement` não está aplicada na base activa: as chaves fiscais abaixo estão documentadas no código/QA, mas não persistidas como relações de requisito nesta base. O INE não tem associação fiscal apropriada.
+
+As cópias e derivados em `storage/app/private/geo-ine-review-*` incluem `review.csv`, JSON de revisão/proveniência e extracções de layout. Foram contados por nome, tamanho e hash, não examinados nem tratados como fontes públicas. Permanecem privados; não são duplicados documentais a publicar.
+
+## Fontes fiscais e correspondência com regras
+
+### Referências oficiais e auxiliares sem ficheiro local
+
+O registo de fontes fiscais contém **25 URLs únicos**. A reconciliação integral dessas referências encontra-se em [fiscal-source-references-2026-10-06.csv](fiscal-source-references-2026-10-06.csv), separada do inventário de ficheiros para não contar uma URL como documento importado. Cada linha conserva o rótulo e endereço exactos, indica que a referência não prova a existência de um binário local e especifica o destino: obter/conferir a fonte, sem activar regras. Algumas URLs apontam para fichas/catálogos e outras para PDFs; quando um PDF coincidir com um dos quatro ficheiros já inventariados, só deve ser contado uma vez pelo SHA-256, após comparação dos bytes.
+
+As 25 referências cobrem: catálogos da AT e INSS; cadeia do CIVA (Leis 3/2012, 13/2016, 16/2020, 22/2022, 3/2025 e 10/2025 e rectificação BR 89/2017); BR 248/2025; pauta aduaneira e e-Viajante; câmbio do Banco de Moçambique; anúncio governamental e fichas INM dos BR 186/187 de 2026; microfinanças (Decreto 50/2024, BM e BR 134/2024); e reprodução auxiliar da Lei 12/2025. A existência de URL oficial não significa que o texto integral foi obtido ou analisado: o estado individual descrito no registo oficial prevalece. A cópia de terceiro é explicitamente auxiliar, nunca evidência primária.
+
+Fontes primárias presentes no acervo activo: Lei 22/2022, BR 248/2025 e BR 89/2017. O código de importação/revisão identifica a Lei 22/2022, art. 17(1), e as Leis 7–12/2025 no BR 248; as relações activas à data desta leitura são somente tags gerais pendentes, não relações persistidas a requisitos `fiscal.*`. A associação candidata do BR 89/2017 também permanece pendente. A identificação de artigos é evidência documental, não validação da incidência, vigência integral ou fórmula.
+
+| Componente | Fonte localizada/ligada | Lacuna documental ou de regra | Estado de uso |
+|---|---|---|---|
+| IVA | Lei 22/2022 art. 17(1); Lei 10/2025 e Lei 3/2025 dentro do BR 248; cadeia de 2016/2017 descrita no registo oficial | O PDF da republicação integral Lei 13/2016 e cópia legível/autêntica dos anexos rectificados continuam inacessíveis; faltam os binários/efeitos consolidados das alterações 2020 e regulamentos publicados/anunciados em 2026 | Um ponto textual de 16% está identificado no documento; sem cálculo fiscal integrado |
+| IRPC | Lei 12/2025, arts. e dispositivos indicados na revisão do BR 248 | Código-base consolidado, regulamentos 2026, retenções, mais-valias digitais/estabelecimento estável por contexto e casos dourados completos não estão arquivados neste lote | Sem regra activada |
+| ISPC | Lei 9/2025 e republicação anexa da Lei 5/2009 no BR 248 | Diploma-base/regulamento integral legível e alterações regulamentares de 2026; elegibilidade e exclusões por operação ainda carecem de consolidação | Sem regra activada; não confundir com regimes simplificados de IVA/IRPS |
+| IRPS | Lei 11/2025 no BR 248 | Código-base consolidado, regulamentos 2026, residência e categorias com alcance/obrigações por período | Sem regra activada; retenção mensal não representa apuramento anual |
+| INSS | Referências externas ao RSSO Decreto 51/2017 e alterações, incluindo Decreto 56/2024, em registo de fontes | Nenhum diploma binário está no arquivo fiscal; taxas/base do regime geral e TCP, limites e vigência ainda não foram conferidos | Sem regra activada; taxas 4%/3% continuam sintéticas/não aprovadas |
+| Retenções | Dispositivos diversos nas Leis 11/2025 e 12/2025 citadas no BR 248 | Falta matriz por natureza, residência, convenção, evento de retenção, documento e vigência; não há fundamento suficiente para uma regra universal | Sem regra activada |
+| ICE | Lei 7/2025, incluindo tabela anexa, citada no BR 248 | Código completo, produto/unidade/base/taxa, transições e regulamentação recente não estão reconciliados em arquivo executável | Sem regra activada |
+| Aduaneiro | Lei 8/2025 e tabela anexa citadas no BR 248 | Pauta de serviço 2026 por código, origem/preferência, classificação, unidade, câmbio aduaneiro e regras da base de IVA importação não foram arquivados e comparados campo a campo | Sem regra activada; sem código pautal não calcular DA |
+| Benefícios/incentivos | Lei 4/2009 é citada por cópia consolidada antiga em inventário anterior, actualmente inacessível | Regime, certificado, actividade, área, período e elegibilidade não têm fontes completas no arquivo disponível | Não há concessão nem benefício automático |
+
+O registo `simulation_tax_rule_versions` não mostrou versões no contexto de leitura da base activa. Independentemente dessa consulta, `RunSimulation` não invoca `SimulationTaxParameterResolver` nem `SimulationTaxRuleExpression` para calcular IRPC, IVA, ISPC, IRPS, INSS, retenções ou aduaneiro; a aritmética genérica introduzida pelo utilizador continua separada. A governança DSL/DAG, o resolvedor temporal e os cenários de QA não são regras fiscais activas. Não alterei cálculos, parâmetros, snapshots ou resultados nesta revisão.
+
+Material secundário `Reforma fiscal de 2026.docx`: foi analisado em revisão anterior como resumo e reconciliado com partes do BR 248; não é tratado como diploma e o binário não está acessível agora. PDF da pauta AT 2026, regulamentos BR 186/187, cópias integrais dos códigos, anexos de pauta/ICE, diplomas INSS, convenções, regulamentos e fontes cambiais permanecem não arquivados/não analisados integralmente, conforme a pendência individual acima ou o registo de fontes oficiais.
+
+## Fluxo reutilizável de importação em lote
+
+Foi criado `documents:import-batch`, que consome um manifesto JSON e ficheiros dentro de `storage/app/private/document-import-staging`. Sem `--apply`, calcula hash, valida MIME/tamanho e pré-visualiza documentos novos, hashes já existentes, duplicados no próprio lote e conflitos de identidade/versão. Com `--apply --actor=`, exige autor real, copia para armazenamento privado, cria rascunho interno em quarentena, regista revisão e auditoria, e coloca a análise antimalware na fila. Não publica, não confirma direitos, autenticidade ou vigência e não exige associações para consulta. Associação a módulo/requisito é uma etapa posterior na gestão. Não há fallback para paths fora do staging; máximo de 10 MiB por ficheiro e 100 entradas por lote.
+
+Uso operacional:
+
+```powershell
+New-Item -ItemType Directory -Force storage/app/private/document-import-staging/lote-20261006
+# Copiar os ficheiros de origem para esta pasta e criar manifest.json conforme o exemplo abaixo.
+php artisan documents:import-batch lote-20261006/manifest.json
+php artisan documents:import-batch lote-20261006/manifest.json --apply --actor=
+```
+
+Cada item exige `external_id`, `relative_path`, `title` e `issuer`; recomenda-se indicar também nome, tipo, edição, número, data, fonte HTTPS, referência e idioma. O formato está exemplificado em [document-import-manifest.example.json](document-import-manifest.example.json). Pré-visualizar e resolver conflitos antes de aplicar. Depois de importar: aguardar scan; completar metadados e proveniência; definir direitos/classificação segundo a autorização aplicável; associar a módulos; publicar explicitamente apenas após as verificações. Repetições pelo mesmo hash não criam documentos ou revisões duplicados.
+
+## Verificações executadas e pendências
+
+- Base activa conferida por leitura: 4 documentos/recursos; 4 `active`/`clean`; 4 hashes analisados iguais aos armazenados; 0 publicados; direitos `unconfirmed`; classificação `internal`.
+- Lote fiscal existente: `php artisan documents:import-legal-source-archive` em pré-visualização; quatro itens `reused/reconciled`, todos `published=no`; nenhuma escrita.
+- Nova importação genérica testada apenas na QA isolada definida por `phpunit.xml`: host `127.0.0.1`, porta `55441`, base `investmoz_test`, sem `DB_URL`; porta confirmada aberta. `php artisan test tests/Feature/ImportDocumentBatchTest.php`: **2 testes, 23 asserções aprovados**, cobrindo pré-visualização sem escrita, rascunho privado, autoria/auditoria, scan enfileirado, repetição idempotente, actor ausente e path traversal.
+- `php -l` no comando e teste: aprovado. `git diff --check`: sem erros de whitespace; Git reportou apenas avisos de conversão CRLF em ficheiros já modificados.
+- Nenhuma migration foi aplicada. Nenhum registo ou estado de publicação activo foi alterado pela pré-visualização. Não foi executado `--apply` no lote real desta revisão, pois os quatro documentos já existem.
+- Pendência para disponibilização pública dos recursos 1–4: entrar como gestor elegível em `/management/portal-services` e executar o botão indicado; esta é a operação autenticada existente, não uma nova autorização. Depois confirmar pesquisa, ficha e download em `/documentos`.
+- Pendências do corpus: localizar/fornecer individualmente os dez itens inacessíveis listados no CSV; verificar vigência, completude e associação artigo por artigo antes de qualquer regra. O relatório não considera inventário de prompts como acervo jurídico.
+
+## Rollback
+
+Não houve migration nem mutação na base activa nesta etapa. O novo comando só grava com `--apply`; os documentos ficam privados e em quarentena técnica. Para reverter a alteração de código, retirar `app/Console/Commands/ImportDocumentBatch.php`, o respectivo teste e esta regra de projecto, preservando quaisquer registos ou ficheiros criados por lotes reais até reconciliação. Não apagar staging/originais nem executar `migrate:rollback` como atalho.
diff --git a/docs/investmoz/document-import-manifest.example.json b/docs/investmoz/document-import-manifest.example.json
new file mode 100644
index 00000000..bd364f68
--- /dev/null
+++ b/docs/investmoz/document-import-manifest.example.json
@@ -0,0 +1,21 @@
+{
+ "documents": [
+ {
+ "external_id": "issuer-document-number-edition-v1",
+ "relative_path": "lote-aaaammdd/original.pdf",
+ "file_name": "original.pdf",
+ "title": "Título exacto conforme à capa/ficha do documento",
+ "issuer": "Entidade emitente identificada",
+ "document_number": "Número do diploma ou referência, se existir",
+ "document_date": "2026-01-01",
+ "language": "pt",
+ "version_label": "Edição/data identificada",
+ "category": "legislation",
+ "topic": "tema",
+ "document_type": "legislation",
+ "source_url": "https://example.org/source",
+ "source_reference": "Origem identificada; autenticidade e vigência independentes por rever.",
+ "description": "Descrição factual sem afirmar aplicabilidade ou elegibilidade."
+ }
+ ]
+}
diff --git a/docs/investmoz/document-requirement-associations-2026-10-06.md b/docs/investmoz/document-requirement-associations-2026-10-06.md
new file mode 100644
index 00000000..508325a2
--- /dev/null
+++ b/docs/investmoz/document-requirement-associations-2026-10-06.md
@@ -0,0 +1,66 @@
+# Associações documentais a requisitos — 06/10/2026
+
+## Âmbito e resultado
+
+Foi concluído o formulário administrativo e a relação entre documentos e identificadores técnicos de requisito. Estas relações são exclusivamente evidência documental: não criam obrigações legais, não aprovam interpretação ou vigência e não activam nem alteram qualquer fórmula, parâmetro ou cálculo fiscal.
+
+Não existia um catálogo de requisitos documentais fiscais no esquema actual. O catálogo técnico criado em `config/document-requirements.php` usa chaves estáveis `fiscal.*`; cada chave identifica um assunto do simulador e o diploma associado, não uma regra legal executável. A interface administrativa selecciona uma chave da lista; o servidor rejeita chaves fora do catálogo. Cada ligação tem estado `pending` ou `confirmed`. `confirmed` significa que a citação foi localizada no ficheiro indicado; a aplicação jurídica continua explicitamente pendente.
+
+A migração aditiva `2026_10_06_200000_add_document_requirement_evidence_links` acrescenta o tipo `requirement`, o estado de conferência e o tipo de revisão `evidence_linked`. Registos anteriores recebem `pending`; nenhum foi promovido em massa. A reversão recusa remover relações ou revisões deste tipo enquanto existirem. O importador reconcilia ligações em falta em recursos já importados, sem duplicar ficheiros, e cria uma revisão do catálogo.
+
+## Referências do BR n.º 248/2025, Suplemento n.º 1
+
+O PDF de 21 páginas foi descarregado do catálogo/WebDAV oficial da Autoridade Tributária e comparado com a cópia arquivada: 440.018 bytes e SHA-256 `CCF2A8DEECA820F2DD076B4B2141B1EFE3F6A806568C3A587CA59A8E92F4C251`. A ficha da Imprensa Nacional identifica a edição. A leitura directa foi feita no texto extraído do próprio PDF, com localização por página. O número do boletim, isoladamente, não é usado como referência de requisito.
+
+| Chave técnica | Diploma identificado dentro do BR | Dispositivos citados no texto primário | Páginas PDF | Estado da associação |
+|---|---|---|---:|---|
+| `fiscal.ice.lei_7_2025` | Lei n.º 7/2025, de 29 de Dezembro, alteração ao Código do ICE | Lei, arts. 1–5 e tabela anexa; art. 1 prorroga a aplicação das taxas até 2027 e art. 5 indica entrada em vigor em 01/01/2026 | 1–2 | Citação localizada; vigência consolidada, regulamentação, alcance da tabela e aprovação fiscal pendentes |
+| `fiscal.customs.lei_8_2025` | Lei n.º 8/2025, de 29 de Dezembro, alteração à Pauta Aduaneira e Instruções Preliminares | Lei, art. 1 altera os arts. 21(2), 22 e 27 das Instruções Preliminares; Lei, arts. 2–5 e tabela anexa | 2–7 | Citação localizada; classificação por código/origem, preferências, regulamentação e aplicação a operações concretas pendentes |
+| `fiscal.ispc.lei_9_2025` | Lei n.º 9/2025, de 29 de Dezembro, alteração ao ISPC | Lei, arts. 1–7 e republicação anexa da Lei n.º 5/2009 | 8–11 | Citação localizada; interpretação, regulamento, elegibilidade e aprovação fiscal pendentes |
+| `fiscal.iva.lei_10_2025` | Lei n.º 10/2025, de 29 de Dezembro, alteração ao CIVA | Lei, arts. 1–4; no texto alterado encontram-se, entre outros, CIVA arts. 18, 19, 21, 23, 25-A, 27 e 32–34 | 12–15 | Citação localizada; consolidação, regulamentação, vigência por dispositivo e aprovação fiscal pendentes |
+| `fiscal.irps.lei_11_2025` | Lei n.º 11/2025, de 29 de Dezembro, alteração ao CIRPS | Lei, arts. 1–4; o texto alterado inclui CIRPS arts. 21, 22, 48, 50, 54-A, 57, 65 e 65-A | 16–18 | Citação localizada; alcance, transições, regulamentos e aprovação fiscal pendentes |
+| `fiscal.irpc.lei_12_2025` | Lei n.º 12/2025, de 29 de Dezembro, alteração ao CIRPC | Lei, arts. 1–4; o texto alterado inclui CIRPC arts. 3, 5, 20, 62, 67 e 75; Lei, art. 2 adita o CIRPC art. 61-A | 19–20 | Citação localizada; alcance, transições, regulamentos e aprovação fiscal pendentes |
+
+As referências de página/artigo acima foram conferidas no próprio PDF. “Citação localizada” não afirma que todos os dispositivos citados incidam sobre qualquer utilizador, que toda a lei continue vigente sem alterações, nem que exista uma obrigação executável. As seis ligações aparecem no sistema como `applicability=evidence`.
+
+Também existe a chave `fiscal.iva.lei_22_2022` para a Lei n.º 22/2022, art. 17(1), página 3 do PDF republicado. A citação do texto com a taxa de 16% foi localizada, mas a associação documental não altera o motor fiscal. A relação à rectificação de 2017 usa a chave `fiscal.iva.lei_13_2016_rectification_2017` e permanece `pending`.
+
+## Quatro ficheiros: origem e pendências por tipo
+
+| Ficheiro e título | Diploma/conteúdo e origem | Autenticidade | Vigência/actualidade | Possibilidade de divulgação |
+|---|---|---|---|---|
+| `Lei-22-2022-IVA-republicada-BR-250-2-suplemento.pdf` — Lei n.º 22/2022, alteração ao CIVA | [INM — 2.º Suplemento do BR n.º 250](https://www.inm.gov.mz/pt-br/content/assembleia-da-rep%C3%BAblica-suplemento-n%C2%BA-2-de-281222-pag-2252-25-42-br-n%C2%BA-250-boletim-da); cópia também comparada byte a byte com o endpoint oficial AT | Confirmada ao nível da origem e bytes (1.834.284 bytes; hash no registo de migração) | Documento alterador de 2022, marcado `amended`; não é consolidação integral actual. Art. 17(1) conferido, mas período e alterações posteriores devem acompanhar a consulta | A lei é texto oficial. Falta decisão administrativa documentada sobre divulgação desta composição/fac-símile PDF e confirmação de direitos de reutilização do artefacto. O registo continua `internal`, direitos `unconfirmed` |
+| `BR-248-I-Suplemento-1-2025.pdf` — BR n.º 248/2025, Suplemento n.º 1 | [INM — ficha da edição](https://inm.gov.mz/pt-br/content/suplemento-n%C2%BA-1-de-291225-pag-2180-1-20-br-n%C2%BA-248-boletim-da-rep%C3%BAblica-i-serie); PDF comparado byte a byte com WebDAV oficial AT | Confirmada ao nível da edição, origem oficial e bytes (440.018 bytes) | Edição contém seis diplomas distintos (Leis 7–12/2025). Não há um único estado de vigência para o boletim; revisão deve ser por lei/artigo e regulamento | Falta documentar a autorização aplicável à divulgação do ficheiro composto, incluindo a apresentação integral do boletim. Permanece privado com direitos `unconfirmed` |
+| `BR-89-I-Serie-2017-06-08.pdf` — BR n.º 89/2017, aviso de rectificação da Lei n.º 13/2016 | [INM — ficha do BR 89, p. 569](https://www.inm.gov.mz/pt-br/content/assembleia-da-rep%C3%BAblica-br-n%C2%BA-89-de-080617-boletim-da-rep%C3%BAblica-i-serie-p%C3%A1g-569) | A ficha oficial confirma a existência do aviso; o PDF local não foi comparado com exemplar oficial descarregado e a cadeia de custódia não está demonstrada | O aviso altera o CIVA e manda rectificar anexos I–III. Os anexos substitutivos e o efeito temporal ainda não foram cotejados com cópia autêntica; não serve como consolidação | Falta autenticar o binário e esclarecer a divulgação do fac-símile. Mantido privado; associação candidata pendente, não exibida como fundamento confirmado |
+| `INE-Codificador-Divisao-Politico-Administrativa-2012.pdf` — Codificador DPA, edição de 07/06/2012 | [INE — PDF oficial](https://www.ine.gov.mz/documents/20119/44337/Codificador%20da%20Divisao%20Politico%20administrativa.pdf/14aed3d0-9296-c038-dbac-041a5cf40f1d) | Cópia local comparada byte a byte com ficheiro servido pelo INE | Edição de 2012; actualidade de códigos/designações territoriais precisa de confirmação antes de orientar cadastro actual | Falta documentar direitos de reutilização da composição PDF. Mantido privado. Não associado a requisito fiscal |
+
+Nenhum destes quatro ficheiros foi publicado nesta execução. O estado antimalware limpo não é autorização de divulgação. As pendências são por ficheiro e independem umas das outras.
+
+## Revisão e publicação autenticadas
+
+O teste de browser no portal activo confirmou que `http://localhost:8000/documentos` responde 200 e apresenta o estado vazio, sem erros JavaScript; a base activa contém 4 recursos e 0 publicados. A migração nova não foi aplicada na base activa. `/management/portal-services` redirecciona para `/login`, pelo que a revisão autenticada não foi executada nem contornada.
+
+Roteiro para o gestor:
+
+1. Aceder a `http://localhost:8000/login` e entrar normalmente na conta de gestor, completando o MFA configurado.
+2. Abrir `http://localhost:8000/management/portal-services` e localizar cada rascunho pelo título/ficheiro.
+3. Conferir fonte, citação e página. No BR 248, rever cada Lei 7/2025 a 12/2025 individualmente; não usar “BR 248” como referência suficiente.
+4. Em cada ficheiro, decidir separadamente autenticidade, estado temporal e autorização de divulgação. Não preencher direitos como autorizados sem base documentada. Para o BR 89, obter primeiro exemplar autêntico e anexos rectificados.
+5. Guardar associações `requirement` como evidência. “Citação conferida” refere-se apenas ao texto e localização lidos; conservar a nota de que interpretação e aplicação fiscal estão pendentes.
+6. Publicar individualmente apenas depois de os controlos do próprio formulário aceitarem fonte, direitos, classificação pública e scan limpo. A publicação é uma acção própria e auditada; não publicar os quatro por lote.
+7. Após aplicação controlada da migração e publicação, abrir `/documentos`, pesquisar pelo título/número, consultar a ficha e conferir a secção de requisitos documentais relacionados; estes identificadores são relações documentais, não páginas ou obrigações legais autónomas. No simulador, consultar a evidência confirmada apresentada junto da recomendação documental. Descarregar o ficheiro pela ligação protegida. Só então registar evidência positiva no portal activo.
+
+## Validação executada
+
+- Alvo de QA do PHPUnit: PostgreSQL em loopback, porta 55441, base `investmoz_test`; o guard em `tests/TestCase.php` recusa a base activa e não permite fallback por `DB_URL`.
+- `php artisan test tests/Feature/PortalServicesTest.php tests/Feature/DocumentLibrarySchemaCompatibilityTest.php tests/Feature/ImportLegalSourceArchiveTest.php`: **18 testes, 433 afirmações aprovadas**. Cobre validação da chave técnica, estados conferidos/pendentes, importação privada, zero publicação, reconciliação após faltar uma associação e ausência de duplicação na repetição.
+- `npm.cmd run types:check`: aprovado.
+- `npm.cmd run build`: aprovado. Avisos não bloqueantes: chunk `services` acima de 500 kB e dependência opcional `fontaine` não instalada.
+- `php -l` nos ficheiros PHP alterados: aprovado.
+- Portal activo: Playwright em desktop, `/documentos` HTTP 200, título “Documentos e Formulários”, estado vazio visível, sem erros JS. Gestão autenticada redireccionou a `/login`. Não houve associação, publicação ou alteração de dados da base activa.
+
+## Rollback e pendências
+
+Em QA, a migração é reversível apenas antes de existirem relações `requirement` e revisões `evidence_linked`; a migração recusa rollback se encontrar esses dados. Na base activa, nenhuma migração foi aplicada e não há rollback de dados a executar. Para activar a associação no portal activo, rever a migração e preparar/restaurar um backup conforme o procedimento operacional existente; depois correr a migração nominal acima e confirmar `php artisan portal:check-document-library-schema`. Não correr migrações globais.
+
+Pendências: aplicação controlada da migração no ambiente activo; revisão autenticada pelo gestor; decisão documental individual sobre os direitos de divulgação dos quatro artefactos; autenticação e anexos rectificados do BR 89; confirmação da actualidade territorial do codificador INE; e evidência posterior de pesquisa, requisito e download positivos. Nenhuma destas pendências activa cálculos fiscais.
diff --git a/docs/investmoz/fiscal-source-references-2026-10-06.csv b/docs/investmoz/fiscal-source-references-2026-10-06.csv
new file mode 100644
index 00000000..683725bd
--- /dev/null
+++ b/docs/investmoz/fiscal-source-references-2026-10-06.csv
@@ -0,0 +1,26 @@
+"SourceRegister";"Label";"URL";"LocalBinary";"State";"Disposition"
+"docs/investmoz/simulations/official-source-register.md";"Autoridade Tributária — Legislação Fiscal";"https://www.at.gov.mz/legislacao/fiscal";"Não identificado por esta referência; URL registada";"Referência de pesquisa/documentação; não conta como ficheiro importado";"Manter como fonte externa a obter/conferir; não activa regra fiscal"
+"docs/investmoz/simulations/official-source-register.md";"Lei n.º 3/2012 — catálogo INM";"https://inm.gov.mz/pt-br/content/assembleia-da-rep%C3%BAblica-suplemento-n%C2%BA-1-de-230112-pag-34-1-6-br-n%C2%BA-3-boletim-da-rep%C3%BAblica-i";"Não identificado por esta referência; URL registada";"Referência de pesquisa/documentação; não conta como ficheiro importado";"Manter como fonte externa a obter/conferir; não activa regra fiscal"
+"docs/investmoz/simulations/official-source-register.md";"Lei n.º 13/2016 — republicação integral INM";"https://www.inm.gov.mz/pt-br/content/suplemento-n%C2%BA-10-de-301216-pag-1426-259-290-br-n%C2%BA-156-boletim-da-rep%C3%BAblica-i-serie";"Não identificado por esta referência; URL registada";"Referência de pesquisa/documentação; não conta como ficheiro importado";"Manter como fonte externa a obter/conferir; não activa regra fiscal"
+"docs/investmoz/simulations/official-source-register.md";"rectificação — BR 89/2017 INM";"https://inm.gov.mz/pt-br/content/assembleia-da-rep%C3%BAblica-br-n%C2%BA-89-de-080617-boletim-da-rep%C3%BAblica-i-serie-p%C3%A1g-569";"Não identificado por esta referência; URL registada";"Referência de pesquisa/documentação; não conta como ficheiro importado";"Manter como fonte externa a obter/conferir; não activa regra fiscal"
+"docs/investmoz/simulations/official-source-register.md";"Lei n.º 16/2020 — INM";"https://www.inm.gov.mz/pt-br/content/assembleia-da-rep%C3%BAblica-suplemento-n%C2%BA-1-de-231220-pag-2414-1-20-br-n%C2%BA-246-boletim-da";"Não identificado por esta referência; URL registada";"Referência de pesquisa/documentação; não conta como ficheiro importado";"Manter como fonte externa a obter/conferir; não activa regra fiscal"
+"docs/investmoz/simulations/official-source-register.md";"Lei n.º 22/2022 republicada — INM";"https://www.inm.gov.mz/pt-br/content/assembleia-da-rep%C3%BAblica-suplemento-n%C2%BA-2-de-281222-pag-2252-25-42-br-n%C2%BA-250-boletim-da";"Não identificado por esta referência; URL registada";"Referência de pesquisa/documentação; não conta como ficheiro importado";"Manter como fonte externa a obter/conferir; não activa regra fiscal"
+"docs/investmoz/simulations/official-source-register.md";"Lei n.º 3/2025 — INM";"https://www.inm.gov.mz/pt-br/content/assembleia-da-rep%C3%BAblica-suplemento-n%C2%BA-1-de-290525-pag-478-1-2-br-n%C2%BA-95-boletim-da-rep%C3%BAblica";"Não identificado por esta referência; URL registada";"Referência de pesquisa/documentação; não conta como ficheiro importado";"Manter como fonte externa a obter/conferir; não activa regra fiscal"
+"docs/investmoz/simulations/official-source-register.md";"Lei n.º 10/2025 — Parlamento";"https://www.parlamento.mz/wp-content/uploads/2025/05/iva.pdf";"Não identificado por esta referência; URL registada";"Referência de pesquisa/documentação; não conta como ficheiro importado";"Manter como fonte externa a obter/conferir; não activa regra fiscal"
+"docs/investmoz/simulations/official-source-register.md";"INSS — Legislação";"https://www.inss.gov.mz/legislacao/";"Não identificado por esta referência; URL registada";"Referência de pesquisa/documentação; não conta como ficheiro importado";"Manter como fonte externa a obter/conferir; não activa regra fiscal"
+"docs/investmoz/simulations/official-source-register.md";"direitos e obrigações do contribuinte";"https://www.inss.gov.mz/direitos-e-obrigacoes-do-contribuinte/";"Não identificado por esta referência; URL registada";"Referência de pesquisa/documentação; não conta como ficheiro importado";"Manter como fonte externa a obter/conferir; não activa regra fiscal"
+"docs/investmoz/simulations/official-source-register.md";"AT — Pauta Aduaneira de Serviço 2026";"https://www.at.gov.mz/servicos/pauta-aduaneira";"Não identificado por esta referência; URL registada";"Referência de pesquisa/documentação; não conta como ficheiro importado";"Manter como fonte externa a obter/conferir; não activa regra fiscal"
+"docs/investmoz/simulations/official-source-register.md";"bundle público da SPA AT";"https://www.at.gov.mz/assets/index-XEcizcP2.js";"Não identificado por esta referência; URL registada";"Referência de pesquisa/documentação; não conta como ficheiro importado";"Manter como fonte externa a obter/conferir; não activa regra fiscal"
+"docs/investmoz/simulations/official-source-register.md";"Portal e-Viajante / AT";"https://e-viajante.at.gov.mz/";"Não identificado por esta referência; URL registada";"Referência de pesquisa/documentação; não conta como ficheiro importado";"Manter como fonte externa a obter/conferir; não activa regra fiscal"
+"docs/investmoz/simulations/official-source-register.md";"Banco de Moçambique — taxas de referência";"https://www.bancomoc.mz/pt/areas-wrapper/taxas-de-cambio-de-referencia-intradiaria/";"Não identificado por esta referência; URL registada";"Referência de pesquisa/documentação; não conta como ficheiro importado";"Manter como fonte externa a obter/conferir; não activa regra fiscal"
+"docs/investmoz/simulations/official-source-register.md";"INM — Suplemento n.º 1 do BR 248, 29-12-2025";"https://inm.gov.mz/pt-br/content/sum%C3%A1rio-suplemento-n%C2%BA-1-de-291225-pag-2180-1-20-br-n%C2%BA-248-boletim-da-rep%C3%BAblica-i-serie-p%C3%A1g-1";"Não identificado por esta referência; URL registada";"Referência de pesquisa/documentação; não conta como ficheiro importado";"Manter como fonte externa a obter/conferir; não activa regra fiscal"
+"docs/investmoz/simulations/official-source-register.md";"Portal do Governo — comunicado de 30-09-2026";"https://portaldogoverno.gov.mz/governo-introduz-alteracoes-nos-regulamentos-fiscais-para-reforcar-eficiencia-tributaria/";"Não identificado por esta referência; URL registada";"Referência de pesquisa/documentação; não conta como ficheiro importado";"Manter como fonte externa a obter/conferir; não activa regra fiscal"
+"docs/investmoz/simulations/official-source-register.md";"catálogo oficial da INM — Boletins da República";"https://inm.gov.mz/pt-br/bulletin?field_tipo_de_produto_tid_br=43";"Não identificado por esta referência; URL registada";"Referência de pesquisa/documentação; não conta como ficheiro importado";"Manter como fonte externa a obter/conferir; não activa regra fiscal"
+"docs/investmoz/simulations/official-source-register.md";"ficha INM BR 186";"https://inm.gov.mz/pt-br/content/br-n%C2%BA-186-de-300926-boletim-da-rep%C3%BAblica-i-serie";"Não identificado por esta referência; URL registada";"Referência de pesquisa/documentação; não conta como ficheiro importado";"Manter como fonte externa a obter/conferir; não activa regra fiscal"
+"docs/investmoz/simulations/official-source-register.md";"ficha INM BR 187";"https://inm.gov.mz/pt-br/content/br-n%C2%BA-187-de-011026-boletim-da-rep%C3%BAblica-i-serie";"Não identificado por esta referência; URL registada";"Referência de pesquisa/documentação; não conta como ficheiro importado";"Manter como fonte externa a obter/conferir; não activa regra fiscal"
+"docs/investmoz/simulations/official-source-register.md";"BM — licenciamento e legislação do sistema";"https://bancomoc.mz/pt/areas-de-actuacao/licenciamento/licenciamento-de-instituicoes/";"Não identificado por esta referência; URL registada";"Referência de pesquisa/documentação; não conta como ficheiro importado";"Manter como fonte externa a obter/conferir; não activa regra fiscal"
+"docs/investmoz/simulations/official-source-register.md";"Decreto 50/2024 em PDF oficial";"https://www.bancomoc.mz/media/jcdnvi0r/decreto-50-2024-11-de-julho-regulamento-da-lei-das-institui%C3%A7%C3%B5es-de-cr%C3%A9dito-e-sociedades-financeiras.pdf";"Não identificado por esta referência; URL registada";"Referência de pesquisa/documentação; não conta como ficheiro importado";"Manter como fonte externa a obter/conferir; não activa regra fiscal"
+"docs/investmoz/simulations/official-source-register.md";"comunicado cadastral BM de 11-09-2026";"https://bancomoc.mz/media/j2fnsbjp/drl_cadastro-operadores-pt.pdf";"Não identificado por esta referência; URL registada";"Referência de pesquisa/documentação; não conta como ficheiro importado";"Manter como fonte externa a obter/conferir; não activa regra fiscal"
+"docs/investmoz/simulations/official-source-register.md";"sumário oficial INM do BR 134/2024";"https://www.inm.gov.mz/pt-br/content/sum%C3%A1rio-br-n%C2%BA-134-de-110724-boletim-da-rep%C3%BAblica-i-serie-p%C3%A1g-2827";"Não identificado por esta referência; URL registada";"Referência de pesquisa/documentação; não conta como ficheiro importado";"Manter como fonte externa a obter/conferir; não activa regra fiscal"
+"docs/investmoz/simulations/official-source-register.md";"ficha oficial INM do aviso";"https://www.inm.gov.mz/pt-br/content/assembleia-da-rep%C3%BAblica-br-n%C2%BA-89-de-080617-boletim-da-rep%C3%BAblica-i-serie-p%C3%A1g-569";"Não identificado por esta referência; URL registada";"Referência de pesquisa/documentação; não conta como ficheiro importado";"Manter como fonte externa a obter/conferir; não activa regra fiscal"
+"docs/investmoz/simulations/official-source-register.md";"reprodução integral de terceiro, auxiliar";"https://www.lexlink.eu/FileGet.aspx?FileId=2999523";"Não identificado por esta referência; URL registada";"Referência de pesquisa/documentação; não conta como ficheiro importado";"Manter como fonte externa a obter/conferir; não activa regra fiscal"
diff --git a/docs/investmoz/legal-source-library-migration-2026-10-06.md b/docs/investmoz/legal-source-library-migration-2026-10-06.md
index 1d21da18..822f2336 100644
--- a/docs/investmoz/legal-source-library-migration-2026-10-06.md
+++ b/docs/investmoz/legal-source-library-migration-2026-10-06.md
@@ -2,6 +2,8 @@
Data: 06/10/2026. Este relatório substitui, para o estado actual, a observação de biblioteca vazia feita em 05/10. A migração foi aplicada à base local `investemoz` depois de um backup novo; não houve alteração aos PDFs originais.
+**Adenda de associações documentais (06/10/2026):** o estado posterior do formulário, chaves técnicas de requisito, conferência de artigos do BR 248, revisão individual dos quatro ficheiros, testes QA e verificação actual do portal estão em [document-requirement-associations-2026-10-06.md](document-requirement-associations-2026-10-06.md). A adenda confirma 4 recursos na base activa, 0 publicados e que a nova migração de relações ainda não foi aplicada nessa base. Os valores de 3 ficheiros/11 ligações abaixo descrevem a etapa anterior desta migração, não o estado actual.
+
## Resultado actual
Foram copiados três PDFs do arquivo existente para o armazenamento privado do portal e registados em `documents` e `portal_resources`. A importação é transaccional e repetível; a segunda execução reutilizou os três registos pelos hashes, sem criar documentos, recursos, ligações ou revisões duplicados.
diff --git a/resources/js/pages/investment/business-marketplace.tsx b/resources/js/pages/investment/business-marketplace.tsx
index 710fea0a..4ed57786 100644
--- a/resources/js/pages/investment/business-marketplace.tsx
+++ b/resources/js/pages/investment/business-marketplace.tsx
@@ -2,29 +2,38 @@ import { Link, router } from "@inertiajs/react";
import { useState } from "react";
import { Frame, button, card, input } from "./shared";
import { TerritoryFields, type TerritoryOption } from "@/components/investment-territory";
+import { businessModalities } from "./business-types";
type PublicBusiness = {
- id: string; slug: string; title: string; objective?: string | null; stage?: string | null;
+ id: string; slug: string; title: string; modality: string; objective?: string | null; stage?: string | null;
amount_min?: string | null; amount_max?: string | null; currency?: string | null;
sector?: { name: string; slug: string } | null; province?: string | null;
};
type PaginationLink = { url: string | null; label: string; active: boolean };
-type Props = { results: { data: PublicBusiness[]; links?: PaginationLink[] | { first: string | null; last: string | null; prev: string | null; next: string | null }; meta?: { links?: PaginationLink[]; total?: number } }; filters: { q?: string; province_id?: string; district_id?: string; admin_post_id?: string; locality_id?: string }; territorySelection?: TerritoryOption[] };
+type Props = { results: { data: PublicBusiness[]; links?: PaginationLink[] | { first: string | null; last: string | null; prev: string | null; next: string | null }; meta?: { links?: PaginationLink[]; total?: number } }; filters: { q?: string; modality?: string; province_id?: string; district_id?: string; admin_post_id?: string; locality_id?: string }; territorySelection?: TerritoryOption[] };
export default function BusinessMarketplace({ results, filters, territorySelection = [] }: Props) {
const [query, setQuery] = useState(filters.q ?? "");
+ const [modality, setModality] = useState(filters.modality ?? "");
const [territory, setTerritory] = useState>({ province_id: filters.province_id ?? "", district_id: filters.district_id ?? "", admin_post_id: filters.admin_post_id ?? "", locality_id: filters.locality_id ?? "" });
- const returnQuery = new URLSearchParams({ ...(query ? { q: query } : {}), ...territory }).toString();
+ const returnQuery = new URLSearchParams({ ...(query ? { q: query } : {}), ...(modality ? { modality } : {}), ...territory }).toString();
const paginationLinks = results.meta?.links ?? (Array.isArray(results.links) ? results.links : []);
return (
Explore oportunidades aprovadas para publicação. A informação está sujeita a diligência independente.
- Versão {z.version_no}. Outra pessoa gestora deve rever antes da
- publicação. Guarde primeiro as alterações.
+ Versão {z.version_no}. Confirme os dados, a fonte e a localização antes
+ de publicar. A publicação não confirma estado operacional, disponibilidade
+ para investimento ou benefícios fiscais. Guarde primeiro as alterações.
(
+ ].filter((v) => v !== 'requirement' || requirementAssociationsEnabled).map((v) => (
))}
@@ -891,6 +901,20 @@ export default function Manage({
+ {disclosureAuthorization && (
+
+
Autorização expressa de disponibilização documental
+
Registada em {new Date(disclosureAuthorization.occurred_at).toLocaleString()}; origem: instrução do utilizador, sem atribuição a gestor. Abrange consulta e download dos documentos listados e não certifica autenticidade ou vigência, nem activa cálculos fiscais.
A acção mantém a autenticação de gestão e só publica ficheiros activos cujo hash corresponda ao resultado antimalware e que tenham fonte identificada. As situações de fonte e vigência são preservadas e apresentadas ao público. Registos que falhem estes controlos ficam pendentes individualmente.