resolvemos o problema dos ZEE e ZFI
tests / ci (push) Canceled after 0s

This commit is contained in:
2026-10-07 09:08:43 +02:00
parent e65cb26901
commit 59d108a576
37 changed files with 1514 additions and 102 deletions
+10
View File
@@ -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.
@@ -0,0 +1,276 @@
<?php
namespace App\Console\Commands;
use App\Jobs\ScanDocumentForMalware;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;
use RuntimeException;
use Throwable;
class ImportDocumentBatch extends Command
{
protected $signature = 'documents:import-batch {manifest : JSON manifest inside storage/app/private/document-import-staging} {--apply : Register private drafts and queue malware scans} {--actor= : Existing user ID responsible for an applied import}';
protected $description = 'Preview or idempotently import a document batch into the existing private library';
private const MAX_BYTES = 10485760;
public function handle(): int
{
$staging = storage_path('app/private/document-import-staging');
$manifestPath = realpath($staging.DIRECTORY_SEPARATOR.$this->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=<existing-user-id>; 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<string, mixed> */
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<string, mixed> $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']),
]))));
}
}
@@ -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]);
}
@@ -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)
+211 -37
View File
@@ -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'
@@ -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',
@@ -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'),
+1 -1
View File
@@ -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<string,mixed> $input */
public function save(User $actor, array $input, ?string $id = null): ExistingBusiness
+24 -5
View File
@@ -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<string,mixed> */
public function context(int $id, bool $management = false): array
{
+24 -17
View File
@@ -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');
@@ -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'));
@@ -0,0 +1,23 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Support\Facades\DB;
return new class extends Migration
{
public function up(): void
{
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 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)))");
}
};
@@ -0,0 +1,47 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Support\Facades\DB;
return new class extends Migration
{
private 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',
];
public function up(): void
{
$this->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<string> $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.'))');
}
};
@@ -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.
@@ -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."
1 RecordType Path FileName SizeBytes SHA256 TitleOrSubject DuplicateGroup Disposition Status Notes
2 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.
3 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.
4 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.
5 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.
6 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.
7 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.
8 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.
9 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.
10 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.
11 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.
12 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.
13 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.
14 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.
15 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.
16 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.
17 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.
18 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.
19 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.
20 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.
21 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.
22 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.
23 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.
24 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.
25 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.
26 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.
27 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.
28 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.
29 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.
30 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.
31 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.
32 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.
33 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.
34 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.
35 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.
36 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.
37 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.
38 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.
39 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.
40 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.
41 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.
42 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.
43 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.
44 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.
45 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.
46 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.
47 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.
48 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.
49 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.
50 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.
51 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.
52 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.
53 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.
54 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.
55 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.
56 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.
57 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.
58 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.
59 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.
60 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.
61 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.
62 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.
63 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.
64 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.
65 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.
66 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.
67 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.
68 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.
69 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.
70 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.
71 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.
72 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.
73 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.
74 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.
75 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.
76 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.
77 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.
78 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.
@@ -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=<ID_EXISTENTE>`, 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=<ID_EXISTENTE>
```
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.
@@ -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."
}
]
}
@@ -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.
@@ -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"
1 SourceRegister Label URL LocalBinary State Disposition
2 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
3 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
4 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
5 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
6 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
7 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
8 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
9 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
10 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
11 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
12 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
13 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
14 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
15 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
16 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
17 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
18 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
19 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
20 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
21 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
22 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
23 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
24 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
25 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
26 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
@@ -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.
@@ -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<Record<string, string>>({ 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 (
<Frame title="Oportunidades de negócio">
<p>Explore oportunidades aprovadas para publicação. A informação está sujeita a diligência independente.</p>
<form className="flex flex-wrap gap-3" onSubmit={(event) => {
event.preventDefault();
router.get("/negocios", { q: query || undefined, ...territory }, { preserveState: true, preserveScroll: true });
router.get("/negocios", { q: query || undefined, modality: modality || undefined, ...territory }, { preserveState: true, preserveScroll: true });
}}>
<label className="sr-only" htmlFor="business-search">Pesquisar negócios</label>
<input id="business-search" className={input} value={query} maxLength={120} onChange={(event) => setQuery(event.target.value)} />
<label className="grid gap-1">
<span>Tipo de oportunidade</span>
<select className={input} value={modality} onChange={(event) => setModality(event.target.value)}>
<option value="">Todos os tipos</option>
{Object.entries(businessModalities).map(([value, label]) => <option key={value} value={value}>{label}</option>)}
</select>
</label>
<TerritoryFields initial={territorySelection} onChange={(path) => setTerritory({ province_id: path[0]?.id ?? "", district_id: path[1]?.id ?? "", admin_post_id: path[2]?.id ?? "", locality_id: path[3]?.id ?? "" })} />
<button className={button}>Pesquisar</button>
</form>
@@ -33,6 +42,7 @@ export default function BusinessMarketplace({ results, filters, territorySelecti
<ul className="grid gap-4 md:grid-cols-2">
{results.data.map((record) => (
<li className={card} key={record.id}>
<p>{businessModalities[record.modality] ?? record.modality}</p>
<h2 className="text-xl font-bold"><Link className="underline" href={`/negocios/${record.slug}${returnQuery ? `?${returnQuery}` : ""}`}>{record.title}</Link></h2>
<p>{[record.sector?.name, record.province, record.stage].filter(Boolean).join(" · ")}</p>
{(record.amount_min || record.amount_max) && <p>{record.amount_min ?? "—"} – {record.amount_max ?? "—"} {record.currency}</p>}
@@ -1,8 +1,9 @@
import { Link } from "@inertiajs/react";
import { Frame, button, card } from "./shared";
import { businessModalities } from "./business-types";
type Business = {
slug: string; title: string; objective?: string | null; stage?: string | null; activity_code?: string | null;
slug: string; title: string; modality: string; objective?: string | null; stage?: string | null; activity_code?: string | null;
amount_min?: string | null; amount_max?: string | null; available_percentage?: string | null; currency?: string | null;
province?: string | null; district?: string | null; locality?: string | null; sector?: { name: string; slug: string } | null;
};
@@ -12,6 +13,7 @@ export default function BusinessOpportunity({ record, returnUrl = "/negocios", i
<Frame title={record.title}>
<Link href={returnUrl} className="underline">Voltar às oportunidades</Link>
<section className={`${card} space-y-4`}>
<p>{businessModalities[record.modality] ?? record.modality}</p>
<p>{[record.sector?.name, record.stage, record.activity_code].filter(Boolean).join(" · ")}</p>
{(record.province || record.district || record.locality) && <p>{[record.locality, record.district, record.province].filter(Boolean).join(", ")}</p>}
{(record.amount_min || record.amount_max) && <p>Montante indicativo: {record.amount_min ?? "—"} – {record.amount_max ?? "—"} {record.currency}</p>}
@@ -51,6 +51,7 @@ export const businessStates: Record<string, string> = {
};
export const businessModalities: Record<string, string> = {
company_sale: 'Venda da empresa',
company_purchase: 'Compra ou aquisição de empresa',
participation_sale: 'Venda de participação',
capital_increase: 'Aumento de capital',
financial_investor: 'Investidor financeiro',
+17 -1
View File
@@ -206,7 +206,11 @@ export default function Catalogue({
<Link className={card} href="/zonas?category=zfi">Fichas de ZFI</Link>
<Link className={card} href="/zonas?category=industrial_park">Parques industriais</Link>
<Link className={card} href="/carteira?kind=project">Projectos âncora documentados</Link>
<Link className={card} href="/negocios">Negócios e participações publicados</Link>
<Link className={card} href="/negocios?modality=company_purchase">Comprar uma empresa</Link>
<Link className={card} href="/negocios?modality=company_sale">Vender uma empresa</Link>
<Link className={card} href="/negocios?modality=real_estate">Imobiliário</Link>
<Link className={card} href="/negocios?modality=land_right">Terrenos e direitos fundiários</Link>
<Link className={card} href="/negocios">Todos os negócios publicados</Link>
</nav>
<p>
Combine contexto territorial, dimensão e necessidades. As fichas documentais e as
@@ -308,6 +312,18 @@ export default function Catalogue({
</select>
</Field>
</div>
{(form.data.zee_id || form.data.zfi_id || form.data.park_id) && (() => {
const selectedId = form.data.zee_id || form.data.zfi_id || form.data.park_id;
const selectedZone = references.zones?.find((zone) => zone.id === selectedId);
return selectedZone ? (
<p role="status" className={card}>
Enquadramento seleccionado: {selectedZone.name}.{' '}
<Link className="underline" href={`/zonas/${selectedZone.id}`}>
Abrir ficha e oportunidades associadas
</Link>
</p>
) : null;
})()}
<section className="grid gap-4 sm:grid-cols-2 lg:grid-cols-4">
<Field label="Estado de preparação">
<select
+9 -6
View File
@@ -32,7 +32,7 @@ type Zone = {
area_value?: string | null;
area_unit?: string | null;
area_components?: unknown[];
source_evidence?: unknown[];
source_evidence?: { title: string; url: string; source_type: string; supports: string; source_date?: string | null }[];
aliases?: string[];
activities?: { name: string; status: string; source_reference?: string | null }[];
resources?: { name: string; status: string; source_reference?: string | null }[];
@@ -431,10 +431,11 @@ function Editor({
}}>Guardar outra localização</button>
</section>
<section className={card}>
<h2 className="text-xl font-bold">Revisão independente</h2>
<h2 className="text-xl font-bold">Publicação informativa</h2>
<p>
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.
</p>
<div className="flex gap-4">
<button
@@ -446,7 +447,7 @@ function Editor({
})
}
>
Validar e publicar
Publicar zona
</button>
<button
className={button}
@@ -598,7 +599,7 @@ function Editor({
<p>Categoria física: {z.physical_category ?? "Por confirmar"} · Regime jurídico: {z.legal_regime ?? "Por confirmar"}{zoneExtensionsAvailable ? ` · Desenvolvimento: ${z.development_status ?? "Por confirmar"} · Disponibilidade: ${z.availability_status ?? "Não informada"}` : ""}</p>
{z.area_value && <p>Área documentada: {z.area_value} {z.area_unit}</p>}
{zoneExtensionsAvailable && !!z.area_components?.length && <section className={card}><h2 className="font-bold">Áreas por âmbito</h2><ul>{z.area_components.map((area, index) => <li key={index}>{JSON.stringify(area)}</li>)}</ul></section>}
{zoneExtensionsAvailable && !!z.source_evidence?.length && <section className={card}><h2 className="font-bold">Fontes e o que comprovam</h2><ul>{z.source_evidence.map((evidence, index) => <li key={index}>{JSON.stringify(evidence)}</li>)}</ul></section>}
{zoneExtensionsAvailable && !!z.source_evidence?.length && <section className={card}><h2 className="font-bold">Fontes e o que comprovam</h2><ul>{z.source_evidence.map((evidence) => <li key={evidence.url}><a className="underline" href={evidence.url} target="_blank" rel="noreferrer">{evidence.title}</a>{evidence.source_date ? ` · ${evidence.source_date}` : " · data não indicada"} · {evidence.source_type} · {evidence.supports}</li>)}</ul><p>Esta informação descreve o que a fonte declara; não confirma vigência legal, benefícios, estado operacional ou disponibilidade para investimento.</p></section>}
{z.historical_territory?.name && <p>Localização histórica: {z.historical_territory.name}{z.historical_territory.district ? ` · ${z.historical_territory.district}` : ""}{z.historical_territory.province ? ` · ${z.historical_territory.province}` : ""}</p>}
{z.activities?.length ? <section className={card}><h2 className="text-xl font-bold">Actividades e sectores referidos</h2><ul>{z.activities.map((activity, index) => <li key={`${activity.name}-${index}`}>{activity.name} · {activity.status === "potential" ? "potencial referido; não confirmado como autorizado" : activity.status === "proposed" ? "proposto; não confirmado como autorizado" : activity.status === "authorized" ? "autorizado segundo a fonte indicada" : activity.status === "conditional" ? "condicionado segundo a fonte indicada" : "excluído segundo a fonte indicada"}{activity.source_reference ? ` · ${activity.source_reference}` : ""}</li>)}</ul></section> : null}
{z.resources?.length ? <section className={card}><h2 className="text-xl font-bold">Infra-estruturas e recursos</h2>{z.resources.map((resource, index) => <p key={`${resource.name}-${index}`}>{resource.name} · {resource.status}{resource.source_reference ? ` · ${resource.source_reference}` : ""}</p>)}</section> : null}
@@ -617,6 +618,8 @@ function Editor({
{source && (
<section className={card}>
<h2 className="text-xl font-bold">Fonte e vigência</h2>
{source.validation_status !== "validated" && <p role="status">Referência documental registada; validação jurídica da fonte pendente. A ficha não confirma vigência, benefício, operacionalidade ou disponibilidade.</p>}
{source.validation_status !== "validated" && <p role="status">Referência documental registada; validação jurídica da fonte pendente. A ficha não confirma vigência, benefício fiscal, operacionalidade ou disponibilidade.</p>}
<a
className="underline"
href={source.url}
+3 -1
View File
@@ -26,6 +26,7 @@ type DocumentResource = {
links: {
target_type: string;
target_key: string;
target_label?: string | null;
relationship: string;
applicability: string;
condition_text: string | null;
@@ -83,6 +84,7 @@ export default function DocumentPage({
<div><dt className="font-semibold">Situação documental</dt><dd>{legalStatusLabels[resource.legal_status] ?? resource.legal_status}</dd></div>
<div><dt className="font-semibold">Fonte</dt><dd>{resource.source_confirmation === 'official_confirmed' ? 'Fonte oficial confirmada' : resource.source_confirmation === 'identified' ? 'Origem identificada' : 'Por confirmar'}</dd></div>
</dl>
{resource.source_confirmation === 'identified' && <p className="rounded border border-amber-300 bg-amber-50 p-3" role="note">A origem está identificada, mas a autenticidade desta cópia não foi confirmada independentemente. Consulte a fonte indicada.</p>}
<p className="break-words text-sm">Referência: {resource.source_reference}</p>
{resource.links.length > 0 && <section className="space-y-2">
<h2 className="text-xl font-semibold">Base legal e documentos relacionados no portal</h2>
@@ -90,7 +92,7 @@ export default function DocumentPage({
{resource.links.map((link, index) => {
const destination = link.target_type === 'module' ? modulePaths[link.target_key] : undefined;
return <li key={`${link.target_type}-${link.target_key}-${index}`}>
{destination ? <Link className="underline" href={destination.href}>{destination.label}</Link> : <span>{link.target_type === 'requirement' ? `Requisito documental: ${link.target_key}` : `${link.target_type}: ${link.target_key}`}</span>}
{destination ? <Link className="underline" href={destination.href}>{destination.label}</Link> : <span>{link.target_type === 'requirement' ? `Requisito documental: ${link.target_label ?? link.target_key}` : `${link.target_type}: ${link.target_key}`}</span>}
{' · '}{link.applicability === 'evidence' ? 'evidência documental' : link.applicability}
{link.condition_text && <span> · Condição: {link.condition_text}</span>}
{link.basis_reference && <span> · Referência documental (não é regra fiscal): {link.basis_reference}</span>}
+25 -1
View File
@@ -80,6 +80,7 @@ type ResourceForm = Omit<
| 'applicability'
| 'condition_text'
| 'basis_reference'
| 'review_status'
> & {
document_id: string;
title: string;
@@ -158,10 +159,18 @@ export default function Manage({
malwareHealth,
malwareCounts,
requirementCatalogue,
requirementAssociationsEnabled,
disclosureAuthorization,
}: {
documents: DocumentFile[];
resources: Resource[];
requirementCatalogue: { key: string; label: string; tax: string }[];
requirementAssociationsEnabled: boolean;
disclosureAuthorization: {
external_id: string;
occurred_at: string;
resources: { id: number; title: string; published: boolean; rights_status: string }[];
} | null;
territories: { id: string; name: string; location_type: string }[];
incompleteCount: number;
possibleDuplicates: number;
@@ -691,6 +700,7 @@ export default function Manage({
Relacionar o mesmo documento a vários módulos não cria
cópias.
</p>
{!requirementAssociationsEnabled && <p role="status" className="text-sm">As associações documentais a requisitos serão activadas após a migração de compatibilidade do catálogo.</p>}
{form.data.associations.map((association, index) => (
<div
className="grid gap-2 rounded border p-3 sm:grid-cols-2"
@@ -721,7 +731,7 @@ export default function Manage({
'investment_type',
'legal_source',
'requirement',
].map((v) => (
].filter((v) => v !== 'requirement' || requirementAssociationsEnabled).map((v) => (
<option key={v}>{v}</option>
))}
</select>
@@ -891,6 +901,20 @@ export default function Manage({
</div>
</form>
{disclosureAuthorization && (
<section className="space-y-3 rounded border border-emerald-700 bg-emerald-50 p-5" aria-labelledby="disclosure-authorization-title">
<h2 id="disclosure-authorization-title" className="text-xl font-bold">Autorização expressa de disponibilização documental</h2>
<p>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.</p>
<ul className="list-disc pl-6">
{disclosureAuthorization.resources.map((resource) => <li key={resource.id}>#{resource.id} · {resource.title} · {resource.published ? 'já publicado' : `rascunho (${resource.rights_status})`}</li>)}
</ul>
<p className="text-sm">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.</p>
<button type="button" className={button} disabled={disclosureAuthorization.resources.every((resource) => resource.published)} onClick={() => router.post('/management/portal-services/disclosure-authorization/apply', {}, { preserveScroll: true })}>
Aplicar autorização e disponibilizar documentos elegíveis
</button>
</section>
)}
<h2 className="text-xl font-bold">
Registos, pendências e duplicados por rever
</h2>
+7 -1
View File
@@ -58,6 +58,7 @@ type Resource = {
links: {
target_type: string;
target_key: string;
target_label?: string | null;
relationship: string;
applicability: string;
condition_text: string | null;
@@ -1081,6 +1082,11 @@ export default function Services(p: Props) {
: 'Fonte por confirmar'}
</dd>
</div>
{r.source_confirmation === 'identified' && (
<div className="sm:col-span-2 rounded border border-amber-300 bg-amber-50 p-3" role="note">
A origem está identificada, mas a autenticidade desta cópia não foi confirmada independentemente. Consulte a fonte indicada.
</div>
)}
<div>
<dt className="font-semibold">
Vigência
@@ -1139,7 +1145,7 @@ export default function Services(p: Props) {
</Link>
) : (
<>
{link.target_type}: {link.target_key}
{link.target_type === 'requirement' ? `Requisito documental: ${link.target_label ?? link.target_key}` : `${link.target_type}: ${link.target_key}`}
</>
)}{' '}
· {link.relationship} ·{' '}
+1
View File
@@ -48,6 +48,7 @@ Route::middleware(['auth', 'verified'])->group(function () {
});
Route::middleware(['auth', 'verified', 'can:manage-portal'])->group(function () {
Route::get('/management/portal-services', [PortalServicesController::class, 'manage'])->name('services.manage');
Route::post('/management/portal-services/disclosure-authorization/apply', [PortalServicesController::class, 'applyDocumentDisclosureAuthorization'])->name('services.disclosure-authorization.apply');
Route::get('/management/documents/{id}/download', [PortalServicesController::class, 'downloadForManager'])->whereUuid('id')->name('documents.manage.download');
Route::post('/management/portal-services/upload', [PortalServicesController::class, 'upload'])->middleware('throttle:10,1')->name('services.upload');
Route::post('/management/documents/{id}/malware/retry', [DocumentMalwareController::class, 'retry'])->whereUuid('id')->middleware('throttle:10,1')->name('documents.malware.retry');
@@ -67,8 +67,53 @@ class BusinessMarketplacePublicTest extends TestCase
$this->assertDatabaseCount('interest_requests', 1);
}
public function test_public_catalogue_filters_by_existing_opportunity_modality_and_opens_response_flow(): void
{
$business = $this->publishedBusiness('real_estate');
$companySale = $this->publishedBusiness('company_sale');
$landRight = $this->publishedBusiness('land_right');
$this->getJson('/api/v1/business-opportunities?modality=real_estate')->assertOk()
->assertJsonCount(1, 'data')->assertJsonPath('data.0.modality', 'real_estate');
$this->getJson('/api/v1/business-opportunities?modality=company_sale')->assertOk()
->assertJsonCount(1, 'data')->assertJsonPath('data.0.modality', 'company_sale');
$this->getJson('/api/v1/business-opportunities?modality=land_right')->assertOk()
->assertJsonCount(1, 'data')->assertJsonPath('data.0.modality', 'land_right');
$this->get('/negocios?modality=real_estate')->assertOk()
->assertInertia(fn ($page) => $page->component('investment/business-marketplace')
->where('filters.modality', 'real_estate')->has('results.data', 1));
$this->get('/negocios/'.$business['public_slug'])->assertOk()
->assertInertia(fn ($page) => $page->component('investment/business-opportunity')
->where('record.modality', 'real_estate'));
$this->get('/negocios/'.$business['public_slug'].'/manifestar-interesse')->assertOk()
->assertInertia(fn ($page) => $page->component('investment/business-interest')
->where('record.modality', 'real_estate'));
$this->get('/negocios?modality=company_sale')->assertOk()
->assertInertia(fn ($page) => $page->component('investment/business-marketplace')
->where('filters.modality', 'company_sale')->has('results.data', 1));
$this->get('/negocios/'.$companySale['public_slug'])->assertOk()
->assertInertia(fn ($page) => $page->component('investment/business-opportunity')
->where('record.modality', 'company_sale'));
$this->get('/negocios?modality=land_right')->assertOk()
->assertInertia(fn ($page) => $page->component('investment/business-marketplace')
->where('filters.modality', 'land_right')->has('results.data', 1));
$this->get('/negocios/'.$landRight['public_slug'])->assertOk()
->assertInertia(fn ($page) => $page->component('investment/business-opportunity')
->where('record.modality', 'land_right'));
$this->postJson('/api/v1/interest-requests', [
'existingBusinessId' => $business['id'], 'modality' => 'real_estate',
'fullName' => 'Synthetic investor', 'email' => 'portal-integration-test@gmail.com', 'phone' => '+258 840000000',
'investorType' => 'empresa', 'country' => 'MZ', 'sectorInterest' => 'Imobiliário',
'message' => 'Synthetic interest in the published real estate opportunity.',
'privacyConsent' => true,
])->assertCreated();
$this->assertDatabaseHas('interest_requests', ['existing_business_id' => $business['id'], 'modality' => 'real_estate', 'status' => 'received']);
}
/** @return array{id:string,public_slug:string} */
private function publishedBusiness(): array
private function publishedBusiness(string $modality = 'company_sale'): array
{
$id = (string) Str::uuid();
$slug = 'public-synthetic-'.$id;
@@ -77,7 +122,7 @@ class BusinessMarketplacePublicTest extends TestCase
'id' => $id,
'code' => 'SYN-'.strtoupper(substr(str_replace('-', '', $id), 0, 12)),
'owner_user_id' => User::factory()->create()->id,
'modality' => 'company_sale',
'modality' => $modality,
'status' => 'published',
'classification' => 'confidential',
'version_no' => 1,
@@ -195,4 +195,43 @@ class ExistingBusinessIntakeTest extends TestCase
$this->assertStringContainsString('Synthetic holder', $snapshot);
$this->actingAs(User::factory()->create())->getJson($this->base.'/'.$first)->assertForbidden();
}
public function test_company_sale_real_estate_and_land_right_have_distinct_private_subject_records(): void
{
$owner = User::factory()->create();
$this->actingAs($owner);
$cases = [
['modality' => 'company_sale', 'kind' => 'company', 'label' => 'Synthetic company sale'],
['modality' => 'real_estate', 'kind' => 'property', 'label' => 'Synthetic commercial property'],
['modality' => 'land_right', 'kind' => 'land_right', 'label' => 'Synthetic declared land right'],
];
foreach ($cases as $case) {
$businessId = $this->postJson($this->base, [
'modality' => $case['modality'],
'classification' => 'confidential',
'reason' => 'Synthetic modality-specific QA draft',
'payload' => ['title' => $case['label'], 'wizard_version' => '2', 'wizard_step' => '6'],
])->assertCreated()->json('id');
$this->get('/investidor/negocios/'.$businessId)->assertOk()
->assertInertia(fn ($page) => $page->component('investment/business')
->where('record.modality', $case['modality'])
->has('formSchema')
->where('objectKinds', fn ($kinds) => $kinds->contains($case['kind'])));
$objectId = $this->postJson($this->base.'/'.$businessId.'/objects', [
'kind' => $case['kind'],
'version_no' => 1,
'reason' => 'Synthetic subject record for QA',
'payload' => ['label' => $case['label'], 'source_reference' => 'Synthetic source reference'],
])->assertCreated()->assertJsonPath('verification_status', 'pending')->json('id');
$this->getJson($this->base.'/'.$businessId)->assertOk()
->assertJsonPath('record.modality', $case['modality'])
->assertJsonPath('objects.0.id', $objectId)
->assertJsonPath('objects.0.kind', $case['kind'])
->assertJsonPath('objects.0.verification_status', 'pending');
}
}
}
@@ -82,6 +82,19 @@ class ExistingBusinessWizardTest extends TestCase
$this->assertStringNotContainsString('synthetic@example.test', $raw);
}
public function test_company_purchase_is_a_distinct_existing_business_modality(): void
{
config(['existing-business.enabled' => true]);
$owner = User::factory()->create();
$this->actingAs($owner)->postJson('/api/v1/existing-business/listings', [
'modality' => 'company_purchase', 'classification' => 'confidential', 'reason' => 'Prepare a company acquisition listing',
'payload' => ['title' => 'Private company acquisition search', 'wizard_version' => '1', 'wizard_step' => '1'],
])->assertCreated()->assertJsonPath('modality', 'company_purchase');
$this->assertDatabaseHas('existing_businesses', ['owner_user_id' => $owner->id, 'modality' => 'company_purchase', 'status' => 'draft']);
$this->get('/negocios?modality=company_purchase')->assertOk()
->assertInertia(fn ($page) => $page->where('filters.modality', 'company_purchase')->has('results.data', 0));
}
public function test_multiple_locations_require_distinct_public_paths_from_the_same_hierarchy(): void
{
config(['existing-business.enabled' => true]);
+88
View File
@@ -0,0 +1,88 @@
<?php
namespace Tests\Feature;
use App\Jobs\ScanDocumentForMalware;
use App\Console\Commands\ImportDocumentBatch;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Bus;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\File;
use Illuminate\Support\Facades\Storage;
use Tests\TestCase;
class ImportDocumentBatchTest extends TestCase
{
use RefreshDatabase;
private string $staging;
private string $batchName;
protected function setUp(): void
{
parent::setUp();
$this->batchName = 'test-'.\Illuminate\Support\Str::uuid();
$this->staging = storage_path('app/private/document-import-staging/'.$this->batchName);
File::ensureDirectoryExists($this->staging);
Storage::fake('local');
Bus::fake();
}
protected function tearDown(): void
{
File::deleteDirectory($this->staging);
parent::tearDown();
}
public function test_preview_is_read_only_and_batch_import_is_private_idempotent_and_attributed(): void
{
$pdf = "%PDF-1.4\nfixture document\n%%EOF";
file_put_contents($this->staging.'/source.pdf', $pdf);
$record = [
'external_id' => 'test-document-v1', 'relative_path' => $this->batchName.'/source.pdf',
'title' => 'Documento de teste', 'issuer' => 'Entidade de teste', 'file_name' => 'documento.pdf',
'source_url' => 'https://example.test/documento.pdf', 'source_reference' => 'URL de teste; sem validação legal.',
'document_number' => 'TEST-1', 'document_date' => '2026-01-01', 'language' => 'pt',
'version_label' => 'edição de teste', 'category' => 'general', 'topic' => 'test', 'document_type' => 'guide',
];
file_put_contents($this->staging.'/manifest.json', json_encode(['documents' => [$record, $record]], JSON_THROW_ON_ERROR));
$actor = \App\Models\User::factory()->create();
$this->artisan(new ImportDocumentBatch(), ['manifest' => $this->batchName.'/manifest.json'])->assertSuccessful();
$this->assertDatabaseCount('documents', 0);
$this->assertDatabaseCount('portal_resources', 0);
$this->assertSame([], Storage::disk('local')->allFiles('portal-documents'));
$this->artisan(new ImportDocumentBatch(), ['manifest' => $this->batchName.'/manifest.json', '--apply' => true, '--actor' => (string) $actor->id])->assertSuccessful();
$this->assertDatabaseCount('documents', 1);
$this->assertDatabaseCount('portal_resources', 1);
$document = DB::table('documents')->firstOrFail();
$this->assertSame($actor->id, $document->uploaded_by);
$this->assertSame('quarantined', $document->status);
$this->assertSame('internal', $document->data_classification);
$this->assertSame('pending', $document->malware_scan_status);
$this->assertDatabaseHas('portal_resources', ['published' => false, 'rights_status' => 'unconfirmed', 'created_by' => $actor->id]);
$this->assertDatabaseHas('audit_events', ['actor_user_id' => $actor->id, 'action' => 'private_document_batch_imported']);
Bus::assertDispatchedTimes(ScanDocumentForMalware::class, 1);
$this->artisan(new ImportDocumentBatch(), ['manifest' => $this->batchName.'/manifest.json', '--apply' => true, '--actor' => (string) $actor->id])->assertSuccessful();
$this->assertDatabaseCount('documents', 1);
$this->assertDatabaseCount('portal_resources', 1);
$this->assertDatabaseCount('audit_events', 1);
Bus::assertDispatchedTimes(ScanDocumentForMalware::class, 1);
}
public function test_apply_refuses_missing_actor_and_source_path_traversal(): void
{
file_put_contents($this->staging.'/source.pdf', "%PDF-1.4\nfixture\n%%EOF");
$record = ['external_id' => 'test-document', 'relative_path' => '..\\..\\..\\..\\.env', 'title' => 'Test', 'issuer' => 'Test'];
file_put_contents($this->staging.'/manifest.json', json_encode(['documents' => [$record]], JSON_THROW_ON_ERROR));
$this->artisan(new ImportDocumentBatch(), ['manifest' => $this->batchName.'/manifest.json', '--apply' => true])->assertFailed();
$this->assertDatabaseCount('documents', 0);
$this->artisan(new ImportDocumentBatch(), ['manifest' => $this->batchName.'/manifest.json'])->assertFailed();
$this->assertDatabaseCount('documents', 0);
}
}
+12 -2
View File
@@ -54,9 +54,10 @@ class ImportLegalSourceArchiveTest extends TestCase
$this->assertDatabaseCount('documents', $expectedResources);
$this->assertDatabaseCount('portal_resources', $expectedResources);
$this->assertDatabaseCount('portal_resource_links', 19);
$this->assertDatabaseCount('portal_resource_links', 8, ['target_type' => 'requirement']);
$this->assertSame(8, DB::table('portal_resource_links')->where('target_type', 'requirement')->count());
$this->assertDatabaseCount('portal_resource_revisions', $expectedResources);
$this->assertCount($expectedResources, Storage::disk('local')->allFiles('portal-documents'));
$this->assertSame($expectedResources, DB::table('documents')->where('source_system', 'legal-source-archive')
->where('status', 'active')->where('data_classification', 'internal')->where('rights_status', 'unconfirmed')
->where('malware_scan_status', 'clean')->whereColumn('sha256', 'malware_scanned_sha256')->count());
@@ -93,8 +94,17 @@ class ImportLegalSourceArchiveTest extends TestCase
$this->assertDatabaseCount('documents', $expectedResources);
$this->assertDatabaseCount('portal_resources', $expectedResources);
$this->assertDatabaseCount('portal_resource_links', 19);
$this->assertDatabaseCount('portal_resource_links', 8, ['target_type' => 'requirement']);
$this->assertSame(8, DB::table('portal_resource_links')->where('target_type', 'requirement')->count());
$this->assertDatabaseCount('portal_resource_revisions', $expectedResources);
$this->assertCount($expectedResources, Storage::disk('local')->allFiles('portal-documents'));
$br248 = DB::table('portal_resources')->where('document_number', 'BR n.º 248/2025, Suplemento n.º 1')->firstOrFail();
DB::table('portal_resource_links')->where('resource_id', $br248->id)->where('target_type', 'requirement')
->where('target_key', 'fiscal.irpc.lei_12_2025')->delete();
$this->artisan('documents:import-legal-source-archive', ['--apply' => true])->assertSuccessful();
$this->assertDatabaseHas('portal_resource_links', ['resource_id' => $br248->id, 'target_type' => 'requirement',
'target_key' => 'fiscal.irpc.lei_12_2025', 'review_status' => 'confirmed']);
$this->assertDatabaseCount('portal_resource_links', 19);
$this->assertDatabaseCount('portal_resource_revisions', $expectedResources + 1);
}
}
+34
View File
@@ -149,6 +149,40 @@ class InvestmentTerritoryTest extends TestCase
$this->assertNotContains($national->id, $service->query([])->pluck('id')->all());
}
public function test_opportunity_cluster_and_corridor_filters_use_their_public_opportunity_relations(): void
{
$country = $this->location('country');
$province = $this->location('province', $country);
$otherProvince = $this->location('province', $country);
foreach ([$country, $province, $otherProvince] as $territoryId) {
DB::table('geo_locations')->where('id', $territoryId)->update(['source_system' => 'cartografia-workbook',
'external_id' => '964a187ad32ea553b9e112cd159caa90b900050c42b7329594a75bcd798a96ff:'.$territoryId,
'data_classification' => 'public']);
}
$sector = Sector::create(['name' => 'Public relation test', 'slug' => 'public-relation-test', 'is_active' => true]);
$corridorId = (string) Str::uuid();
DB::table('development_corridors')->insert(['id' => $corridorId, 'code' => 'QA-CORRIDOR', 'name' => 'QA public corridor',
'status' => 'active', 'data_classification' => 'public', 'created_at' => now(), 'updated_at' => now()]);
$clusterId = (string) Str::uuid();
DB::table('economic_clusters')->insert(['id' => $clusterId, 'code' => 'QA-CLUSTER', 'name' => 'QA public cluster',
'sector' => 'QA', 'status' => 'active', 'data_classification' => 'public', 'created_at' => now(), 'updated_at' => now()]);
$opportunity = Opportunity::create(['sector_id' => $sector->id, 'reference' => 'QA-CORRIDOR-OPP', 'slug' => 'qa-corridor-opp',
'title' => 'QA opportunity linked to corridor', 'status' => 'published', 'published_at' => now()->subMinute(), 'primary_territory_id' => $province]);
DB::table('opportunity_corridors')->insert(['opportunity_id' => $opportunity->id, 'corridor_id' => $corridorId]);
DB::table('opportunity_clusters')->insert(['opportunity_id' => $opportunity->id, 'cluster_id' => $clusterId]);
$this->getJson('/api/v1/investment/references')->assertOk()->assertJsonFragment(['id' => $corridorId, 'name' => 'QA public corridor'])
->assertJsonFragment(['id' => $clusterId, 'name' => 'QA public cluster']);
$this->getJson('/api/v1/investment/search?dimension=corridor&province_id='.$province)->assertOk()
->assertJsonPath('total', 1)->assertJsonPath('data.0.id', $opportunity->id);
$this->getJson('/api/v1/investment/search?corridor_id='.$corridorId.'&province_id='.$province)->assertOk()
->assertJsonPath('total', 1)->assertJsonPath('data.0.id', $opportunity->id);
$this->getJson('/api/v1/investment/search?cluster_id='.$clusterId)->assertOk()
->assertJsonPath('total', 1)->assertJsonPath('data.0.id', $opportunity->id);
$this->getJson('/api/v1/investment/search?dimension=corridor&province_id='.$otherProvince)->assertOk()
->assertJsonPath('total', 0)->assertJsonCount(0, 'data');
}
public function test_cluster_territory_uses_only_public_validated_project_links_and_project_locations(): void
{
$country = $this->location('country');
+62 -6
View File
@@ -44,7 +44,7 @@ class InvestmentZoneTest extends TestCase
$this->getJson('/api/v2/investment/zones')->assertJsonPath('records.total', 0);
$this->postJson($url.'/'.$id, [...$payload, '_method' => 'PUT', 'version_no' => 1, 'name' => 'Changed synthetic zone'])->assertOk()->assertJsonPath('version_no', 2);
$this->putJson($url.'/'.$id, [...$payload, 'version_no' => 1])->assertConflict();
$this->postJson($url.'/'.$id.'/publish', ['version_no' => 2])->assertForbidden();
$this->postJson($url.'/'.$id.'/publish', ['version_no' => 2])->assertUnprocessable();
$this->actingAs($this->manager())->postJson($url.'/'.$id.'/publish', ['version_no' => 2])->assertUnprocessable();
$this->assertDatabaseCount('investment_zone_revisions', 2);
$snapshot = DB::table('investment_zone_revisions')->where('zone_id', $id)->where('version_no', 1)->value('snapshot');
@@ -123,8 +123,7 @@ class InvestmentZoneTest extends TestCase
public function test_documented_general_industrial_park_can_publish_without_special_regime_or_operator_and_filters_match_csv(): void
{
$editor = $this->manager();
$publisher = $this->manager();
config(['investment-zones.publishers' => [$publisher->email]]);
config(['investment-zones.publishers' => []]);
$country = $this->geoNode('country', 'Synthetic country for park', null);
$province = $this->geoNode('province', 'Synthetic province for park', $country);
$evidence = [['title' => 'Synthetic municipal project page', 'url' => 'https://example.test/munhava', 'source_type' => 'institutional',
@@ -136,7 +135,7 @@ class InvestmentZoneTest extends TestCase
'source_evidence' => $evidence, 'area_components' => [['type' => 'pilot', 'value' => 50, 'unit' => 'ha', 'status' => 'reported',
'source_reference' => 'Synthetic source reports pilot area; not available land.', 'source_date' => null]],
])->assertCreated()->json('id');
$this->actingAs($publisher)->postJson('/api/v2/investment/admin/zones/'.$id.'/publish', ['version_no' => 1])->assertOk();
$this->actingAs($editor)->postJson('/api/v2/investment/admin/zones/'.$id.'/publish', ['version_no' => 1])->assertOk();
$this->getJson('/api/v2/investment/zones?development_status=planned')->assertOk()->assertJsonPath('records.total', 1)
->assertJsonPath('records.data.0.id', $id)->assertJsonPath('records.data.0.availability_status', 'not_informed');
$csv = $this->get('/api/v2/investment/zones/export.csv?development_status=planned')->assertOk()->assertHeader('content-type', 'text/csv; charset=UTF-8');
@@ -148,6 +147,55 @@ class InvestmentZoneTest extends TestCase
$this->assertDatabaseHas('investment_zones', ['id' => $id, 'operator_id' => null, 'legal_source_id' => null, 'publication_status' => 'published']);
}
public function test_promoter_sourced_industrial_park_can_be_published_as_informational_only(): void
{
$manager = $this->manager();
config(['investment-zones.publishers' => []]);
$country = $this->geoNode('country', 'Synthetic country for promoter park', null);
$province = $this->geoNode('province', 'Synthetic province for promoter park', $country);
$id = $this->actingAs($manager)->postJson('/api/v2/investment/admin/zones', [
'code' => 'QA-PROMOTER-PARK', 'name' => 'Synthetic promoter-sourced park', 'category' => 'industrial_park',
'physical_category' => 'industrial_park', 'legal_regime' => 'pending', 'development_status' => 'in_development',
'availability_status' => 'not_informed',
'description' => 'Informational project record; legal status, operations, incentives and available land are not confirmed.',
'province_id' => $province,
'source_evidence' => [['title' => 'Promoter project page', 'url' => 'https://example.test/promoter-park', 'source_type' => 'promoter',
'supports' => 'The promoter identifies the project and province; no legal designation or land availability claim.', 'source_date' => null]],
])->assertCreated()->json('id');
$this->postJson('/api/v2/investment/admin/zones/'.$id.'/publish', ['version_no' => 1])->assertOk();
$this->getJson('/api/v2/investment/zones?category=industrial_park')->assertOk()
->assertJsonPath('records.total', 1)->assertJsonPath('records.data.0.id', $id)
->assertJsonPath('records.data.0.legal_regime', 'pending')
->assertJsonPath('records.data.0.availability_status', 'not_informed')
->assertJsonPath('records.data.0.source_evidence.0.source_type', 'promoter');
$this->assertDatabaseHas('investment_zones', ['id' => $id, 'publication_status' => 'published', 'legal_regime' => 'pending',
'operator_id' => null, 'legal_source_id' => null]);
}
public function test_officially_sourced_zee_can_be_published_informationally_by_the_authenticated_author(): void
{
$manager = $this->manager();
config(['investment-zones.publishers' => []]);
$country = $this->geoNode('country', 'Synthetic country for ZEE', null);
$province = $this->geoNode('province', 'Synthetic province for ZEE', $country);
$id = $this->actingAs($manager)->postJson('/api/v2/investment/admin/zones', [
'code' => 'QA-INFO-ZEE', 'name' => 'Synthetic reported ZEE', 'category' => 'zee',
'physical_category' => 'economic_zone', 'legal_regime' => 'zee',
'description' => 'Informational record; legal status and incentives remain unconfirmed.', 'province_id' => $province,
'source_evidence' => [['title' => 'Institutional source', 'url' => 'https://example.test/zee', 'source_type' => 'institutional',
'supports' => 'Reports the zone identity and province; does not confirm current legal status or incentives.', 'source_date' => null]],
])->assertCreated()->json('id');
$this->postJson('/api/v2/investment/admin/zones/'.$id.'/publish', ['version_no' => 1])->assertOk();
$this->getJson('/api/v2/investment/zones?category=zee')->assertOk()->assertJsonPath('records.total', 1)
->assertJsonPath('records.data.0.id', $id);
$this->get('/oportunidades?province_id='.$province)->assertOk()
->assertInertia(fn ($page) => $page->has('references.zones', 1)->where('references.zones.0.id', $id));
$this->assertDatabaseHas('investment_zones', ['id' => $id, 'editor_id' => $manager->id, 'reviewer_id' => $manager->id,
'publication_status' => 'published', 'operator_id' => null, 'legal_source_id' => null]);
}
public function test_zone_record_separates_physical_type_and_legal_regime_and_searches_multiple_reviewed_territories_once(): void
{
$editor = $this->manager();
@@ -280,9 +328,11 @@ class InvestmentZoneTest extends TestCase
$organization = (string) Str::uuid();
DB::table('organizations')->insert(['id' => $organization, 'name' => 'Synthetic operator', 'organization_type' => 'private']);
$sector = Sector::create(['slug' => 'qa-zone-sector', 'name' => 'Synthetic sector', 'is_active' => true]);
$opportunity = Opportunity::create(['sector_id' => $sector->id, 'reference' => 'QA-ZONE', 'slug' => 'qa-zone', 'title' => 'Synthetic investment', 'status' => 'published', 'published_at' => now()->subMinute()]);
$zoneCountry = $this->geoNode('country', 'Synthetic Country for zone link', null);
$zoneProvince = $this->geoNode('province', 'Synthetic Province for zone link', $zoneCountry);
$opportunity = Opportunity::create(['sector_id' => $sector->id, 'reference' => 'QA-ZONE', 'slug' => 'qa-zone', 'title' => 'Synthetic investment',
'status' => 'published', 'published_at' => now()->subMinute(), 'province' => 'Synthetic Province for zone link',
'geography_scope' => 'specific', 'primary_territory_id' => $zoneProvince]);
$zoneIds = [];
foreach (['zee', 'zfi'] as $category) {
$this->actingAs($editor);
@@ -313,6 +363,11 @@ class InvestmentZoneTest extends TestCase
$this->getJson('/api/v1/investment/search?dimension=zfi&view=list')->assertOk()->assertJsonPath('total', 1);
$this->getJson('/api/v1/investment/search?dimension=zee&q=not-present')->assertJsonPath('total', 0);
$this->getJson('/api/v1/investment/search?dimension=invalid')->assertUnprocessable();
$this->get('/oportunidades?province_id='.$zoneProvince.'&zee_id='.$zoneIds['zee_id'])->assertOk()
->assertInertia(fn ($page) => $page->component('investment/catalogue')->has('references.zones', 2)
->has('results.data', 1)->where('results.data.0.id', $opportunity->id));
$this->getJson('/api/v2/investment/zones/'.$zoneIds['zee_id'])->assertOk()
->assertJsonPath('opportunities.total', 1)->assertJsonPath('opportunities.data.0.id', $opportunity->id);
$this->getJson('/api/v2/investment/zones')->assertJsonPath('records.total', 2)->assertJsonMissingPath('records.data.0.editor_id');
$this->getJson('/api/v2/investment/zone-map')->assertJsonCount(2, 'features');
$this->seed(InvestmentReferenceSeeder::class);
@@ -382,7 +437,8 @@ class InvestmentZoneTest extends TestCase
$this->assertDatabaseCount('investment_zone_relations', 3);
$this->assertDatabaseCount('investment_items', 0);
config(['investment-zones.publishers' => [], 'investment-zones.legal_reviewers' => [], 'investment-zones.gis_reviewers' => []]);
$this->postJson('/api/v2/investment/admin/zones/'.$id.'/publish', ['version_no' => 1])->assertForbidden();
$this->postJson('/api/v2/investment/admin/zones/'.$id.'/publish', ['version_no' => 1])->assertUnprocessable();
$this->assertDatabaseHas('investment_zones', ['id' => $id, 'publication_status' => 'draft']);
$this->getJson('/api/v2/investment/zones')->assertJsonPath('records.total', 0);
}
+123 -9
View File
@@ -48,6 +48,28 @@ class PortalServicesTest extends TestCase
$this->get('/management/portal-services')->assertRedirect('/login');
}
public function test_document_management_writes_redirect_to_the_management_get_route(): void
{
$manager = User::factory()->create();
config(['portal.managers' => [$manager->email]]);
$this->actingAs($manager);
// A stale Referer can point at the write-only resource endpoint. Both
// validation failures and successful writes must finish at a real GET.
$this->from('/management/portal-services/resources/4')
->post('/management/portal-services/resources/4', ['published' => 'invalid'])
->assertRedirect(route('services.manage'))
->assertSessionHasErrors('published');
$this->get(route('services.manage'))->assertOk()->assertInertia(fn (Assert $page) => $page->component('portal/manage'));
$this->from('/management/portal-services/resources/4')
->post('/management/portal-services/resources', [])
->assertRedirect(route('services.manage'))
->assertSessionHasErrors('document_id');
$this->get('/management/portal-services/resources/4')->assertStatus(405);
}
public function test_support_receipt_is_idempotent_private_and_requires_consent(): void
{
$owner = User::factory()->create();
@@ -110,19 +132,21 @@ class PortalServicesTest extends TestCase
$this->get('/documentos')->assertInertia(fn (Assert $page) => $page->has('resources.data', 0));
$id = $this->registerDocumentResource($document->id, 'Guia de teste');
$this->assertDatabaseHas('portal_resources', ['id' => $id, 'published' => false, 'rights_status' => 'authorized']);
$this->post('/management/portal-services/resources/'.$id, ['published' => true])->assertUnprocessable();
$this->post('/management/portal-services/resources/'.$id, ['published' => true])->assertRedirect(route('services.manage'))->assertSessionHasErrors('published');
// A malware scan releases technical quarantine without changing privacy or editorial publication.
DB::table('documents')->where('id', $document->id)->update(['status' => 'active', 'data_classification' => 'public']);
$this->post('/management/portal-services/resources/'.$id, ['published' => true])->assertSessionHasNoErrors();
$this->post('/management/portal-services/resources/'.$id, ['published' => true])->assertRedirect(route('services.manage'))->assertSessionHasNoErrors();
$this->get('/documentos/'.$id.'/download')->assertOk()->assertDownload('exemplo.txt');
$this->get('/documentos/'.$id.'?back=%2Fdocumentos%3Ftopic%3DInvestimento')->assertInertia(fn (Assert $page) => $page
->component('portal/document')->where('resource.title', 'Guia de teste')->where('backUrl', '/documentos?topic=Investimento')
->has('resource.links', 3)->missing('resource.storage_key')->missing('resource.sha256'));
$this->get('/')->assertInertia(fn (Assert $page) => $page->has('essentialResources', 1));
$this->get('/documentos')->assertInertia(fn (Assert $page) => $page->has('resources.data', 1)->where('resources.data.0.title', 'Guia de teste')->missing('resources.data.0.storage_key'));
$this->get('/documentos?q=Guia%20de%20teste&document_type=guide')->assertInertia(fn (Assert $page) => $page
->component('portal/services')->has('resources.data', 1)->where('resources.data.0.title', 'Guia de teste'));
$this->getJson('/api/v1/documents/recommendations?module=simulator&activity_id=CAE-TEST&stage=review')->assertOk()->assertJsonPath('data.0.title', 'Guia de teste');
$this->put('/management/portal-services/resources/'.$id, [...$this->documentResourcePayload($document->id, 'Guia de teste revisto'), 'metadata_version' => 2, 'description' => 'Resumo editado pelo gestor.'])
->assertSessionHasNoErrors();
->assertRedirect(route('services.manage'))->assertSessionHasNoErrors();
$this->assertDatabaseHas('portal_resources', ['id' => $id, 'published' => true, 'title' => 'Guia de teste revisto']);
$this->assertDatabaseHas('portal_resource_revisions', ['resource_id' => $id, 'revision_no' => 3, 'change_type' => 'metadata_updated']);
DB::table('documents')->where('id', $document->id)->update(['data_classification' => 'internal']);
@@ -175,7 +199,7 @@ class PortalServicesTest extends TestCase
$this->assertDatabaseCount('portal_resource_links', 20);
}
public function test_document_publication_marks_proposals_and_requires_a_confirmed_source(): void
public function test_legislation_with_an_identified_source_can_be_published_for_consultation_without_claiming_confirmation(): void
{
Storage::fake('local');
$manager = User::factory()->create();
@@ -187,12 +211,77 @@ class PortalServicesTest extends TestCase
$this->post('/management/portal-services/resources', $payload)->assertSessionHasNoErrors();
$id = (int) DB::table('portal_resources')->value('id');
DB::table('documents')->where('id', $document->id)->update(['status' => 'active', 'data_classification' => 'public']);
$this->post('/management/portal-services/resources/'.$id, ['published' => true])->assertUnprocessable();
$this->put('/management/portal-services/resources/'.$id, [...$payload, 'metadata_version' => 1, 'source_confirmation' => 'official_confirmed'])
->assertSessionHasNoErrors();
$this->post('/management/portal-services/resources/'.$id, ['published' => true])->assertSessionHasNoErrors();
$this->get('/legislacao')->assertInertia(fn (Assert $page) => $page->where('resources.data.0.legal_status', 'proposed')
->where('resources.data.0.source_confirmation', 'official_confirmed'));
->where('resources.data.0.source_confirmation', 'identified'));
}
public function test_recorded_user_authorization_publishes_only_eligible_document_scope_in_one_manager_action(): void
{
Storage::fake('local');
$manager = User::factory()->create();
config(['portal.managers' => [$manager->email]]);
$this->post('/management/portal-services/disclosure-authorization/apply')->assertRedirect('/login');
$this->actingAs($manager);
$resourceIds = [];
$titles = [
1 => 'Lei n.º 22/2022, de 28 de Dezembro — alteração ao Código do IVA (republicação)',
2 => 'Boletim da República n.º 248/2025 — Suplemento n.º 1',
3 => 'Boletim da República n.º 89, I Série (08-06-2017) — aviso de rectificação da Lei n.º 13/2016',
4 => 'Codificador da Divisão Político-Administrativa (INE, 07-06-2012)',
];
foreach ([1, 2, 3, 4] as $number) {
$this->post('/management/portal-services/upload', [
'file' => UploadedFile::fake()->createWithContent("reference-$number.pdf", "document bytes $number"),
'original_source' => 'https://example.gov.mz/reference',
])->assertSessionHasNoErrors();
$document = DB::table('documents')->where('file_name', "reference-$number.pdf")->firstOrFail();
$resourceIds[] = $this->registerDocumentResource($document->id, $titles[$number], [
'document_type' => $number === 3 ? 'legislation' : 'reference_data',
'legislation' => $number === 3,
'source_confirmation' => $number === 3 ? 'identified' : 'official_confirmed',
'legal_status' => $number === 3 ? 'unknown' : 'historical',
'rights_status' => 'unconfirmed', 'data_classification' => 'internal',
]);
DB::table('documents')->where('id', $document->id)->update([
'status' => 'active', 'malware_scan_status' => 'clean', 'malware_scanned_sha256' => $document->sha256,
]);
}
$this->assertCount(4, $resourceIds);
DB::table('audit_events')->insert([
'source_system' => 'user-instruction', 'external_id' => 'document-repository-disclosure-authorization-2026-10-06-v1',
'data_classification' => 'internal', 'entity_type' => 'portal_document_repository', 'entity_id' => null,
'action' => 'public_disclosure_authorized', 'actor_user_id' => null,
'after_data' => json_encode(['resource_ids' => $resourceIds], JSON_THROW_ON_ERROR),
]);
$this->post('/management/portal-services/disclosure-authorization/apply')
->assertRedirect(route('services.manage'))->assertSessionHasNoErrors();
$this->assertDatabaseCount('portal_resource_revisions', 8);
$this->assertDatabaseCount('audit_events', 13);
$this->assertSame(4, DB::table('portal_resources')->where('published', true)->count());
$this->assertSame(4, DB::table('documents')->where('data_classification', 'public')->where('rights_status', 'authorized')->count());
$this->assertDatabaseHas('portal_resources', ['id' => $resourceIds[2], 'published' => true, 'source_confirmation' => 'identified', 'legal_status' => 'unknown']);
$this->assertDatabaseHas('portal_resource_links', ['resource_id' => $resourceIds[0], 'target_type' => 'requirement',
'target_key' => 'fiscal.iva.lei_22_2022', 'review_status' => 'confirmed', 'applicability' => 'evidence']);
$this->assertDatabaseHas('portal_resource_links', ['resource_id' => $resourceIds[1], 'target_type' => 'requirement',
'target_key' => 'fiscal.iva.lei_10_2025', 'review_status' => 'pending', 'applicability' => 'evidence']);
$this->assertDatabaseHas('portal_resource_links', ['resource_id' => $resourceIds[2], 'target_type' => 'requirement',
'target_key' => 'fiscal.iva.lei_13_2016_rectification_2017', 'review_status' => 'pending', 'applicability' => 'evidence']);
$this->assertDatabaseHas('audit_events', ['source_system' => 'portal-services', 'entity_type' => 'portal_resources',
'actor_user_id' => $manager->id, 'action' => 'published_under_user_authorization']);
$this->get('/documentos')->assertInertia(fn (Assert $page) => $page->has('resources.data', 4));
$this->get('/documentos?q=22%2F2022')->assertInertia(fn (Assert $page) => $page
->has('resources.data', 1)->where('resources.data.0.id', $resourceIds[0]));
$this->get('/documentos?category=Guia&document_type=legislation')->assertInertia(fn (Assert $page) => $page
->has('resources.data', 1)->where('resources.data.0.id', $resourceIds[2]));
$this->get('/documentos/'.$resourceIds[2])->assertInertia(fn (Assert $page) => $page->component('portal/document')
->where('resource.source_confirmation', 'identified')->where('resource.legal_status', 'unknown'));
$this->get('/documentos/'.$resourceIds[0].'/download')->assertOk()->assertDownload('reference-1.pdf');
$this->assertDatabaseHas('portal_resource_links', ['resource_id' => $resourceIds[0], 'target_type' => 'module', 'target_key' => 'simulator']);
$this->post('/management/portal-services/disclosure-authorization/apply')->assertSessionHasNoErrors();
$this->assertDatabaseCount('portal_resource_revisions', 8);
}
public function test_manager_classifies_a_reviewed_document_public_before_separate_publication(): void
@@ -286,7 +375,7 @@ class PortalServicesTest extends TestCase
$resource = DB::table('portal_resources')->firstOrFail();
$this->assertFalse((bool) $resource->published);
$this->assertSame('internal', DB::table('documents')->where('id', $document->id)->value('data_classification'));
$this->post('/management/portal-services/resources/'.$resource->id, ['published' => true])->assertUnprocessable();
$this->post('/management/portal-services/resources/'.$resource->id, ['published' => true])->assertRedirect(route('services.manage'))->assertSessionHasErrors('published');
$this->get('/documentos')->assertInertia(fn (Assert $page) => $page->has('resources.data', 0));
$this->get('/documentos/'.$resource->id)->assertNotFound();
$this->get('/documentos/'.$resource->id.'/download')->assertNotFound();
@@ -347,6 +436,31 @@ class PortalServicesTest extends TestCase
$this->assertDatabaseCount('documents', 1);
}
public function test_public_recommendations_return_only_confirmed_requirement_citations(): void
{
Storage::fake('local');
$manager = User::factory()->create();
config(['portal.managers' => [$manager->email]]);
$this->actingAs($manager)->post('/management/portal-services/upload', [
'file' => UploadedFile::fake()->createWithContent('public-evidence.txt', 'Documentary evidence fixture.'),
])->assertSessionHasNoErrors();
$document = DB::table('documents')->firstOrFail();
$payload = $this->documentResourcePayload($document->id, 'Fontes fiscais QA');
$payload['associations'][] = ['target_type' => 'requirement', 'target_key' => 'fiscal.iva.lei_22_2022', 'relationship' => 'cites',
'applicability' => 'evidence', 'review_status' => 'confirmed', 'basis_reference' => 'Citação fixture, artigo 17(1), página 3.'];
$payload['associations'][] = ['target_type' => 'requirement', 'target_key' => 'fiscal.iva.lei_10_2025', 'relationship' => 'cites',
'applicability' => 'evidence', 'review_status' => 'pending', 'basis_reference' => 'Citação ainda não conferida no fixture.'];
$this->post('/management/portal-services/resources', $payload)->assertSessionHasNoErrors();
$id = (int) DB::table('portal_resources')->where('title', 'Fontes fiscais QA')->value('id');
DB::table('documents')->where('id', $document->id)->update(['status' => 'active', 'data_classification' => 'public']);
$this->post('/management/portal-services/resources/'.$id, ['published' => true])->assertSessionHasNoErrors();
$this->getJson('/api/v1/documents/recommendations?module=simulator')->assertOk()
->assertJsonPath('data.0.documentary_evidence.0.key', 'fiscal.iva.lei_22_2022')
->assertJsonPath('data.0.documentary_evidence.0.status', 'citation_checked_application_pending')
->assertJsonMissing(['key' => 'fiscal.iva.lei_10_2025']);
}
private function registerDocumentResource(string $documentId, string $title, array $overrides = []): int
{
$payload = [...$this->documentResourcePayload($documentId, $title), ...$overrides];